RubyMotion Debugging Guide for iOS and OS X

This article covers how to debug RubyMotion iOS and OS X projects using the builtin debugging facility. RubyMotion apps can be debugged either on the simulator or the device.

At the time of this writing, the debugging experience in RubyMotion is still a work in progress, and this document might change any time to reflect the progresses that have been made in this regard.

1. Synopsis

The RubyMotion debugger for iOS and OS X projects is based on LLDB, the LLVM Debugger.

LLDB is traditionally used to debug programs written in C-based languages, however, RubyMotion brings Ruby support to LLDB, allowing it to connect and introspect RubyMotion processes.

Note
The LLDB support is at this point experimental and also quite low-level. Our goal is to build a higher-level, friendlier debugger on top of LLDB that will provide a better experience for Ruby developers.

This document aims at covering the main features that one might need in order to debug a RubyMotion app with LLDB. This document is not a complete LLDB manual. We highly recommend reading the official LLDB documentation if an exhaustive guide is needed.

Note
In this guide we will use the longhand versions of all debugger commands, but most, if not all, have shorthand versions which you can find in the the official LLDB documentation.

2. Debugging Symbols

The RubyMotion compiler implements the DWARF debugging format metadata for the Ruby language. This allows external programs such as debuggers or profilers to retrieve source-level information about an existing RubyMotion application.

For iOS and OS X apps, the metadata is saved under a .dSYM bundle file at the same level as the .app bundle, in the build directory of your project.

Both development and release modes have debugging symbols, however, as the release mode activates compilation optimizations, the debugging experience will be better under the development mode. For example, in the release mode, certain local variables and arguments might not be accessible in the debugger as they could be optimized.

3. Starting the Debugger

In order to start the debugger, the debug option can be set to any value on the appropriate rake target.

When working with the simulator rake task, the debugger will directly attach itself to the app and replace the interactive shell (REPL).

$ rake simulator debug=1

When working with the device rake task, the build system will start the iOS debugging server on the device then remotely attach the debugger on your shell right after the application has been deployed on the device.

$ rake device debug=1

3.1. Entering Commands Before Starting

By default the application will run right after the debugger is started. Sometimes you might want to perform commands before the application starts.

This can be done by setting the no_continue option to any value. If set, the debugger will not continue to the application and will give you a chance to change the debugging environment (for example, setting a breakpoint). You can type the continue command once you are ready to move on.

$ rake debug=1 no_continue=1
[...]
(lldb) breakpoint set --file hello_view.rb --line 10
Breakpoint 3: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) c
Process 87523 resuming
[...]

3.2. Saving Commands

It is also possible to save on disk commands you want the debugger to perform automatically when running your application.

The debugger will honor the debugger_cmds file in the root directory of your project. If this file exists, its content will be interpreted as a list of debugger commands separated by a newline character.

Only use the name of the file not the full path : hello_view.rb:10 and not app/hello_view.rb:10.

$ echo "breakpoint set --file hello_view.rb --line 10" > debugger_cmds
$ rake debug=1
[...]

4. Managing Breakpoints

To set a breakpoint to a given location in the source code, use the breakpoint command and pass the location where the debugger should break, using the --line option.

As an example, the following command sets a breakpoint on the 10th line of the hello_view.rb file.

(lldb) breakpoint set --file hello_view.rb --line 10

The breakpoint list command can be used to list the breakpoints that have been set in the current debugger environment.

(lldb) breakpoint list
Current breakpoints:
1: name = 'rb_exc_raise', locations = 1, resolved = 1
  1.1: where = Hello`rb_exc_raise, address = 0x00049d70, resolved, hit count = 0

2: name = 'malloc_error_break', locations = 1, resolved = 1
  2.1: where = libsystem_malloc.dylib`malloc_error_break, address = 0x030b47f9, resolved, hit count = 0

3: file = 'hello_view.rb', line = 10, locations = 1, resolved = 1
  3.1: where = Hello`rb_scope__drawRect:__ + 1034 at hello_view.rb:10, address = 0x0000964a, resolved, hit count = 1

As you can see our breakpoint hello_view.rb:10 is right there and is enabled. The breakpoint enable and breakpoint disable commands can respectively enable or disable a given breakpoint using its number.

Since our breakpoint is number 3 in the list, we can disable it like this:

(lldb) breakpoint disable 3
1 breakpoints disabled.

5. Getting the Backtrace

Once you hit a breakpoint, it is often interesting to check out the execution backtrace, which will tell you where the method is called from.

This can be done by using the thread backtrace command.

(lldb) thread backtrace
* thread #1: tid = 0x36e013, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
    frame #1: 0x00009d77 Hello`__unnamed_9 + 231
    frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
    frame #3: 0x024bb800 QuartzCore`+[CATransaction flush] + 52
    frame #4: 0x005eed70 UIKit`_UIApplicationHandleEvent + 683
    frame #5: 0x03add6e1 GraphicsServices`PurpleEventCallback + 46
    frame #6: 0x01da8ffb CoreFoundation`CFRunLoopRunInMode + 123
    frame #7: 0x005ec8be UIKit`-[UIApplication _run] + 840
    frame #8: 0x005eeabb UIKit`UIApplicationMain + 1225
    frame #9: 0x000024cc Hello`main(argc=1, argv=0xbfffec0c) + 156 at main.mm:15

Backtrace frames in your code can be identified with the rb_scope__ prefix and the file:line information.

5.1. Frames

Here, the very first frame in the backtrace is the method defined in the breakpoint location: drawRect:. The other frames below the breakpoint are native iOS calls. As we can see, our drawRect: method is called by the UIView class, which makes sense.

The frame command lets you switch to a specific frame in the backtrace. By default you will be at the top frame (#0), but assuming you want to go down to frame #2, in order to inspect its context, you can type the following command to do so.

(lldb) frame select 2
frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504:
-> 0x65be43:  calll  0x6156b5                  ; UIGraphicsPopContext
   0x65be48:  addl   $108, %esp
   0x65be4b:  popl   %esi
   0x65be4c:  popl   %edi

Obviously it mainly matters when you want to go down to a specific Ruby-defined location in the backtrace, otherwise you’ll only get assembly, as illustrated by the preceding example.

5.2. Threads

The thread backtrace command only returns the backtrace of the current thread. When dealing with a multithreaded program, you may sometimes want to print the backtrace of all running threads, for instance when you are debugging a race condition.

The following command will print the backtrace of all the running threads in the terminal.

(lldb) thread backtrace all
* thread #1: tid = 0x36e013, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
    frame #1: 0x00009d77 Hello`__unnamed_9 + 231
    frame #2: 0x0065be43 UIKit`-[UIView(CALayerDelegate) drawLayer:inContext:] + 504
    frame #3: 0x024bb800 QuartzCore`+[CATransaction flush] + 52
    frame #4: 0x005eed70 UIKit`_UIApplicationHandleEvent + 683
    frame #5: 0x03add6e1 GraphicsServices`PurpleEventCallback + 46
    frame #6: 0x01da8ffb CoreFoundation`CFRunLoopRunInMode + 123
    frame #7: 0x005ec8be UIKit`-[UIApplication _run] + 840
    frame #8: 0x005eeabb UIKit`UIApplicationMain + 1225
    frame #9: 0x000024cc Hello`main(argc=1, argv=0xbfffec0c) + 156 at main.mm:15

  thread #2: tid = 0x36e04e, 0x031bf992 libsystem_kernel.dylib`kevent64 + 10, queue = 'com.apple.libdispatch-manager
    frame #0: 0x031bf992 libsystem_kernel.dylib`kevent64 + 10
    frame #1: 0x02de018e libdispatch.dylib`_dispatch_mgr_invoke + 238
    frame #2: 0x02ddfeca libdispatch.dylib`_dispatch_mgr_thread + 60

  thread #3: tid = 0x36e04f, 0x031bf046 libsystem_kernel.dylib`__workq_kernreturn + 10
    frame #0: 0x031bf046 libsystem_kernel.dylib`__workq_kernreturn + 10
    frame #1: 0x03182dcf libsystem_pthread.dylib`_pthread_wqthread + 372

  thread #4: tid = 0x36e050, 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #0: 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #1: 0x001397b6 Hello`-[RMREPL start] + 134
    frame #2: 0x015567a7 Foundation`-[NSThread main] + 76
    frame #3: 0x01556706 Foundation`__NSThread__main__ + 1275
    frame #4: 0x031815fb libsystem_pthread.dylib`_pthread_body + 144
    frame #5: 0x03181485 libsystem_pthread.dylib`_pthread_start + 130

Similar to switching frames, the debugger will let you switch threads using the thread select command. This can be useful if you want to inspect a specific Ruby method frame in another running thread. The following command will switch the debugger prompt to the thread #4.

(lldb) thread select 4
* thread #4: tid = 0x36e050, 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
    frame #0: 0x031bdd2e libsystem_kernel.dylib`accept$UNIX2003 + 10
libsystem_kernel.dylib`accept$UNIX2003 + 10:
-> 0x31bdd2e:  jae    0x31bdd3e                 ; accept$UNIX2003 + 26
   0x31bdd30:  calll  0x31bdd35                 ; accept$UNIX2003 + 17
   0x31bdd35:  popl   %edx
   0x31bdd36:  movl   29423(%edx), %edx

6. Inspecting Objects

After checking the backtrace, you may want to inspect the objects around. The debugger will let you print them using specialized commands.

6.1. Local Variables

We just hit our breakpoint defined in the drawRect:(rect) method. As you can see from the breakpoint, we are inside a function that accepts two arguments: self and rect. rect is definitely our CGRect argument, but what is self?

In RubyMotion, the self argument is a pointer to the self object exposed in Ruby, which represents a reference to the receiver of the method. In the debugger, self is visible as the first argument of the method.

We can inspect the values of both self and rect by using the print-ruby-object command. This RubyMotion-defined command sends the inspect message to the given object and returns its value. The command can also be called using the pro shortcut which we will use as a convenience.

frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08fa34d0, rect=0x08f8d8a0) + 1034 at hello_view.rb:10
   7   	      end
   8   	      text = "ZOMG!"
   9   	    else
-> 10  	      bgcolor = UIColor.blackColor
   11  	      text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12  	    end
   13
(lldb) print-ruby-object self
#<HelloView:0x8fa34d0>
(lldb) pro rect
#<CGRect origin=#<CGPoint x=0.0 y=0.0> size=#<CGSize width=320.0 height=568.0>>

The list of local variables can be printed using the frame variable command. The list will also include the addresses of each local variable.

(lldb) frame variable
(void *) self = 0x08fa34d0
(void *) rect = 0x08f8d8a0
(void *) bgcolor = 0x08f931c0
(void *) red = 0x00000004
(void *) green = 0x00000004
(void *) blue = 0x00000004
(void *) text = 0x09c55230
(void *) font = 0x00000004

These local variables can also be individually inspected on the terminal by using the pro command.

(lldb) pro bgcolor
#<UICachedDeviceWhiteColor:0x8f931c0>
(lldb) pro text
"Hello RubyMotion!"
(lldb) pro font
nil

6.2. Instance Variables

Instance variables of an object can be printed using the print-ruby-ivar command, or its convenience shortcut pri.

If the command is given two arguments, the first one is the object on which the instance variable will be retrieved, and the second one must be a string representing the instance variable that you want to get. Make sure to include the @ character in the name.

(lldb) pri self "@touches"
2

When called with only one argument, the command assumes that you want to retrieve the given instance variable from self.

(lldb) pri "@touches"
2

7. Control Flow

The next command will continue the execution of the program until the next source-level location. This is usually the very next line in the Ruby source code. This means that the debugger has not yet executed the line that it indicates is the current line, keep this in mind when inspecting variables and their values.

* thread #1: tid = 0x3702a0, 0x0000964a Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1034 at hello_view.rb:10, queue = 'com.apple.main-thread, stop reason = breakpoint 3.1
    frame #0: 0x0000964a Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1034 at hello_view.rb:10
   7   	      end
   8   	      text = "ZOMG!"
   9   	    else
-> 10  	      bgcolor = UIColor.blackColor
   11  	      text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12  	    end
   13
(lldb) next
Process 87162 stopped
* thread #1: tid = 0x3702a0, 0x000096c9 Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1161 at hello_view.rb:11, queue = 'com.apple.main-thread, stop reason = step over
    frame #0: 0x000096c9 Hello`rb_scope__drawRect:__(self=0x08ed1600, rect=0x08d91620) + 1161 at hello_view.rb:11
   8   	      text = "ZOMG!"
   9   	    else
   10  	      bgcolor = UIColor.blackColor
-> 11  	      text = @touches ? "Touched #{@touches} times!" : "Hello RubyMotion!"
   12  	    end
   13
   14  	    bgcolor.set

The continue command will continue the execution of the program until it reaches a breakpoint.

(lldb) continue
Process 87162 resuming

When the program runs, you can always stop its execution and go back to the debugger prompt by typing the control`c+ (^C`) keyboard shortcut.

^C
Process 87162 stopped
[...]
(lldb)

If you want to quit the debugger, just type the quit command and confirm that you want to exit. It will terminate the application and return you back to the shell prompt.

(lldb) quit
Quitting LLDB will detach from one or more processes. Do you really want to proceed: [Y/n] y
$