Intro
Debugging applications is a natural cycle of writing software. One simply cannot anticipate every single problem they are going to run into while using an piece of software.
For rock, since we compile to C, we can use traditional debugging tools like gdb, and the next few sections explain exactly how.
Debug symbols
By default, rock compiles in the debug profile. The corresponding command line
option is -pg
.
Not only will this pass the corresponding option to the C compiler used (gcc, clang, etc.) but it will also:
- Add
#line
directives for debuggers to map back to .ooc files - Keep produced C files around for further inspection.
- On Linux, it’ll add -rdynamic so that all symbols are exported
- On OSX, it’ll run dsymutil so that a
.dSYM
archive will be produced, containing debug symbols.
When releasing a production build of your software, use the release profile instead, using:
rock -pr
This will omit debug symbols.
Fancy backtraces
While the next sections cover using a debugger, which is a prerequisite for pretty much all hardcore problem-solving sections, there is a way to get information about program crashes without using a debugger.
The fancy-backtrace rock extension produces output like this when a program crashes:
[OutOfBoundsException in ArrayList]: Trying to access an element at offset 0, but size is only 0!
[fancy backtrace]
0 fancy_backtrace.c (from C:\msys64\home\amwenger\Dev\rock\bin\fancy_backtrace.DLL)
1 BacktraceHandler backtrace_impl() in lang/Backtrace (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Backtrace.ooc:50)
2 BacktraceHandler backtrace() in lang/Backtrace (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Backtrace.ooc:243)
3 Exception addBacktrace_impl() in lang/Exception (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Exception.ooc:108)
4 Exception addBacktrace() in lang/Exception (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Exception.ooc:212)
5 Exception throw_impl() in lang/Exception (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Exception.ooc:177)
6 Exception throw() in lang/Exception (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Exception.ooc:232)
7 ArrayList get_impl() in structs/ArrayList (at C:/msys64/home/amwenger/Dev/rock/sdk/structs/ArrayList.ooc:82)
8 ArrayList get() in structs/ArrayList (at C:/msys64/home/amwenger/Dev/rock/sdk/structs/ArrayList.ooc:40)
9 __OP_IDX_ArrayList_Int__T() in structs/ArrayList (at C:/msys64/home/amwenger/Dev/rock/sdk/structs/ArrayList.ooc:290)
10 foo() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:32)
11 bar() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:41)
12 App runToo_impl() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:72)
13 App runToo() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:84)
14 __crash_closure403() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:67)
15 __crash_closure403_thunk() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:66)
16 loop() in lang/Abstractions (at C:/msys64/home/amwenger/Dev/rock/sdk/lang/Abstractions.ooc:2)
17 App run_impl() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:65)
18 App run() in crash (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:80)
19 main() in (at C:/msys64/home/amwenger/Dev/rock/test/sdk/lang/crash.ooc:1)
20 crtexe.c (from C:\msys64\home\amwenger\Dev\rock\test\sdk\lang\crash.exe)
21 crtexe.c (from C:\msys64\home\amwenger\Dev\rock\test\sdk\lang\crash.exe)
22 BaseThreadInitThunk (from C:\Windows\system32\kernel32.dll)
23 RtlUserThreadStart (from C:\Windows\SYSTEM32\ntdll.dll)
In the case above,
Fancy backtraces works on Windows, Linux, and OSX, on both 32 and 64-bit machines.
To use it, simply go in the rock directory and do:
make extensions
A few dependencies might be needed, such as binutils-dev
and zlibg1-dev
on
Debian, or a few brew formulas on OSX.
Fancy backtrace principle
Basically, whenever an exception is thrown, a backtrace is captured. It contains a list of frames, e.g. the addresses of the various function calls (as can be seen above).
If an Exception isn’t caught, the program will abort, but before it does, the backtrace captured when the exception was thrown is formatted nicely and printed out to the standard error stream.
Similarly, when the program receives a signal (such as SIGSEGV), a backtrace is printed to help the developer know when things were wrong.
Since fancy-backtrace has more dependencies than rock itself, it’s a little bit harder to build, and that’s why it exists as a dynamic library (a .dll file on Windows, .dylib on OSX, and .so on Linux).
When a program compiled in the debug profile starts up, it attempts to load the library. If it succeeds, it will use it to display friendly stack traces. If it doesn’t, it will fall back to the execinfo interface (which displays only function names, not source files or line numbers), or to… nothing, on Windows.
By default, the fancy_backtrace.{dll,so,dylib} file is copied along to the rock
binary, in ${ROCK_DIST}/bin
. An ooc executable will first look in its own
directory (useful if the application is distributed on a system that doesn’t
have rock), and will then search in the directory where the rock executable
resides (useful on a developer system).
Fancy backtrace configuration
The default setting is to display something as helpful as possible. However, if
one wants unformatted backtraces, one may define the RAW_BACKTRACE
environment variable:
RAW_BACKTRACE=1 ./myprogram
To disable the usage of fancy-backtrace altogether, one may use the
NO_FANCY_BACKTRACE
environment variable:
NO_FANCY_BACKTRACE=1 ./myprogram
Crash course in gdb
GDB, the GNU Debugger, is the canonical tool to debug C applications compiled with gcc (or even clang).
For example, writing this in dog.ooc
Dog: class {
shout: func {
raise("Woops, not implemented yet")
}
}
main: func {
work()
}
work: func {
Dog new() shout()
}
Compiling with rock -pg
gives an executable, dog
, and a folder with C files.
Running
We can run it with gdb like this:
gdb dog
If we wanted to pass arguments we could do
gdb --args dog arg1 arg2 arg3
Inside gdb, we are greeted with a prompt. Typing run
(or r
) for short, followed
by a line feed, runs the program. In this case, it aborts and tells us where it failed:
(gdb) r
Starting program: /Users/amos/Dev/tests/dog
Reading symbols for shared libraries +.............................. done
[Exception]: Woops, not implemented yet
Program received signal SIGABRT, Aborted.
0x00007fff96b82d46 in __kill ()
Getting a backtrace
However, as-is, we don’t know much. So it died in __kill
— that seems to be a system
function on OSX (where this doc was written). How about a nice backtrace instead? Running
backtrace
or simply bt
will give you that:
(gdb) bt
#0 0x00007fff96b82d46 in __kill ()
#1 0x00007fff8edfadf0 in abort ()
#2 0x0000000100007121 in lang_Exception__Exception_throw_impl (this=0x100230030) at Exception.ooc:205
#3 0x00000001000072a3 in lang_Exception__Exception_throw (this=0x100230030) at Exception.ooc:241
#4 0x0000000100008090 in lang_Exception__raise (msg=0x100231600) at Exception.ooc:104
#5 0x0000000100000e9b in dog__Dog_shout_impl (this=0x100238ff0) at dog.c:3
#6 0x0000000100000ef0 in dog__Dog_shout (this=0x100238ff0) at dog.ooc:11
#7 0x0000000100001131 in dog__work () at dog.ooc:12
#8 0x0000000100001102 in main (__argc2=1, __argv3=0x7fff5fbff230) at dog.ooc:8
We have from left to right - the frame number, the address of the function (we can ignore), the name of the function, then the arguments and their values, and then the files where they were defined (if they can be found) along with the line number.
Reading code with context
As expected, we can see ooc line numbers in the backtrace. What if we want to
investigate the code without opening the .ooc file ourselves? We can just place
ourselves in the context of frame 7 with frame 7
or simply f 7
:
(gdb) f 7
#7 0x0000000100001131 in dog__work () at dog.ooc:12
12 Dog new() shout()
Want more context, e.g. the lines of code around? Use list
(or simply l
)
(gdb) l
7 main: func {
8 work()
9 }
10
11 work: func {
12 Dog new() shout()
13 }
(gdb)
Inspecting values
GDB can also print values. For example, going back to frame 2, we can inspect
the exception being thrown by using print
(or p
, for short):
(gdb) f 2
#2 0x0000000100007121 in lang_Exception__Exception_throw_impl (this=0x100230030) at Exception.ooc:205
205 abort()
(gdb) p this
$4 = (lang_Exception__Exception *) 0x100230030
Getting an address is not that useful, though, how about printing the content of an object instead? We can dereference the object from within gdb:
(gdb) p *this
$10 = {
__super__ = {
class = 0x100047e60
},
backtraces = 0x100234f40,
origin = 0x0,
message = 0x100231600
}
What if we want to read the message? Since it’s an ooc String, we’ll need to print the content of the underlying buffer:
(gdb) p *this.message._buffer
$11 = {
__super__ = {
__super__ = {
class = 0x100047490
},
T = 0x1000478c0
},
size = 26,
capacity = 0,
mallocAddr = 0x0,
data = 0x10002fe24 "Woops, not implemented yet"
}
Inspecting generics
Inspecting generics is a bit trickier - one has to cast it directly to the
right type. For example, the Exception
class has a LinkedList of backtraces,
which is a generic type. We can inspect it:
(gdb) p *this.backtraces
$21 = {
__super__ = {
__super__ = {
__super__ = {
__super__ = {
class = 0x10004b2b0
},
T = 0x100047df0
}
},
equals__quest = {
thunk = 0x10001a660,
context = 0x0
}
},
_size = 0,
head = 0x100239f60
}
Not so useful. What about head?
(gdb) p *this.backtraces.head
$22 = {
__super__ = {
class = 0x10004b4a0
},
T = 0x100047df0,
prev = 0x100239f60,
next = 0x100239f60,
data = 0x100238fe0 ""
}
Looks like a node from a doubly-linked list. We’re on the right track! However,
data is printed as a C string (since generics are uint8_t*
under the hood, and
uint8_t
is usually typedef’d to char
). We can cast it:
(gdb) p (lang_types__Pointer) *this.backtraces.head.data
$24 = (lang_types__Pointer) 0x0
Which seems about right, as the exception has not been re-thrown (obviously the example here is rather specific, but the general techniques can be applied to any ooc application).
Breakpoints
What if we want to inspect values somewhere the program wouldn’t stop naturally?
In the program above, we could set up a breakpoint when the constructor of Dog
is called.
It can be non-trivial to determine the C symbol corresponding to an ooc function.
Tab-completion is here to the rescue though - typing break dog_
and then hitting
Tab twice will display a helpful list of symbols:
(gdb) break dog_<TAB><TAB>
dog__Dog___defaults__ dog__Dog___load__ dog__Dog_init dog__Dog_shout dog__work
dog__Dog___defaults___impl dog__Dog_class dog__Dog_new dog__Dog_shout_impl dog_load
Here, we seem to want dog__Dog_new
. As a rule, we have packagename__ClassName_methodName_suffix
.
Setting up the break point does nothing until we run the program:
(gdb) break dog__Dog_new
Breakpoint 1 at 0x100000f3a: file dog.ooc, line 1.
(gdb) r
Starting program: /Users/amos/Dev/tests/dog
Reading symbols for shared libraries +.............................. done
Breakpoint 1, dog__Dog_new () at dog.ooc:1
1 Dog: class {
Stepping
From there, we can investigate as before, with backtrace
, frame
, print
, etc. We can also decide
to step line by line. Using step
will enter the functions being called, whereas next
will skip them
and return to the prompt when the functions have fully executed.
The shorthand for step
is s
, and the shorthand for next is n
. When we step, we can see everything
being executed, including string and object allocation:
(gdb) s
dog__Dog_class () at dog.ooc:25
Line number 25 out of range; dog.ooc has 13 lines.
(gdb)
Line number 26 out of range; dog.ooc has 13 lines.
(gdb)
Line number 27 out of range; dog.ooc has 13 lines.
(gdb)
lang_types__Object_class () at types.ooc:55
55 if(object) {
(gdb)
dog__Dog_class () at dog.ooc:28
Line number 28 out of range; dog.ooc has 13 lines.
(gdb)
Line number 29 out of range; dog.ooc has 13 lines.
(gdb)
lang_String__makeStringLiteral (str=0x10002fe20 "Dog", strLen=3) at String.ooc:377
377 String new(Buffer new(str, strLen, true))
(gdb)
lang_Buffer__Buffer_new_cStrWithLength (s=0x10002fe20 "Dog", length=3, stringLiteral__quest=true) at Buffer.ooc:59
59 init: func ~cStrWithLength(s: CString, length: Int, stringLiteral? := false) {
(gdb)
lang_Buffer__Buffer_class () at Buffer.ooc:157
157 clone: func ~withMinimum (minimumLength := size) -> This {
(gdb)
158 newCapa := minimumLength > size ? minimumLength : size
(gdb)
163 copy
(gdb)
0x00000001000050dd in lang_Buffer__Buffer_new_cStrWithLength (s=0x10002fe20 "Dog", length=3, stringLiteral__quest=true) at Buffer.ooc:59
59 init: func ~cStrWithLength(s: CString, length: Int, stringLiteral? := false) {
(gdb)
lang_types__Class_alloc__class (this=0x100047490) at types.ooc:54
54 object := gc_malloc(instanceSize) as Object
(gdb)
Note that some line numbers seem to be problematic here - but we still get to see which parts of the
code get executed and in which order. Instead of typing s
every time, we can just hit Enter to
re-execute the last command.
When we’re done stepping and just want to resume program execution, we can use continue
(or c
for short).
Quitting
When we’re done running gdb, we can quit with quit
(or q
for short). It might ask for confirmation
if the program is still running, but otherwise, it’s all good.
Attaching to a process
Up to there, we have seen how to run a program from gdb. What if we want to attach gdb to a process that has been
launched somewhere else? Let’s try with this program, sleep.ooc
:
import os/Time
main: func {
while (true) {
doThing()
Time sleepSec(1)
}
}
doThing: func {
"Hey!" println()
}
Compiling it with rock -pg
and running it with ./sleep
prints Hey!
every second, as expected.
To attach to this process, we need to find out its process ID. We can either use the ps
command line
utility, or we can interrupt its execution with Ctrl-Z
(in most shells, like bash, zsh, etc.). You
might see something like this:
amos at coyote in ~/Dev/tests
$ ./sleep
Hey!
Hey!
Hey!
^Z
[1] + 48130 suspended ./sleep
And the process ID is 48130
here. We can attach gdb to that process like this:
gdb attach 48130
When attaching to a process, GDB will pause execution, waiting for orders. Quitting gdb will then detach gdb from the process, which will resume execution.
If you need to quit the process, you can bring it back to the front with the fg
shell
command, then exit it with Ctrl-C
.
Other tips
ooc vs C line numbers
By default, rock will output ‘sourcemaps’, mapping C code back to the original ooc code that generated it. This allows the debugger to display ooc line numbers, as seen above. This behavior can be disabled with:
rock --nolines
In which case gdb will fall back to displaying C line numbers (corresponding to the files generated by rock). This can be useful if you suspect that rock is generating invalid code, or if the ooc line numbers are messed up for some reason.