Cool things

rocky edited this page Dec 2, 2012 · 2 revisions

See also:

Stopping on an exception before it gets raised

Ruby provides for a "raise" event and trepan can stop on that kind of event. (set events determines which events to stop at). To force an exception to be raised in the program use the debugger "raise" command.

trepan will also show you the exception object that will passed back on a raise event. No, I don't have a way to clear the exception. For that, come back later.

  trepan /tmp/ex1.rb 
  -- (/tmp/ex1.rb:1)
  (trepan): step!   # step until the next "exception" event
  #<ZeroDivisionError: divided by 0>
  !! (/tmp/ex1.rb:1)
  (trepan): where
  --> #0 CFUNC Fixnum#/(0) in file /tmp/ex1.rb at line 1
      #1 TOP Object#<top /tmp/ex1.rb> in file /tmp/ex1.rb at line 1

In fact if you step along at a low-enough level of granularity, you can even see the exception object as it gets created:

  trepan /tmp/ex1.rb 
  -- (/tmp/ex1.rb:1)
  (trepan): set trace on
  trace is on.
  (trepan): step until false
  CFUNC Fixnum#/(0)
  C> (/tmp/ex1.rb:1)
  CFUNC Class#new(#<RubyVM::...)
  C> (/tmp/ex1.rb:1)
  CFUNC ZeroDivisionError#initialize(#<RubyVM::...)
  C> (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)
  CFUNC ZeroDivisionError#exception()
  C> (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)
  CFUNC ZeroDivisionError#backtrace()
  C> (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)
  CFUNC ZeroDivisionError#set_backtrace(#<RubyVM::...)
  C> (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)
  #<ZeroDivisionError: divided by 0>
  !! (/tmp/ex1.rb:1)
  <C (/tmp/ex1.rb:1)

Above, trace on shows events without stopping. "step until false" is just one way of making sure the debugger sees events. "finish" would also work. But "continue" causes the debugger to get out of the way and full-speed execution to continue until there is a previously-set VM breakpoint.

ruby-debug can also stop at an exception event, but the exception has to be mentioned beforehand using the "catch" command. In trepan all exceptions are handled by default. To disable, this use "set events" and omit the "raise" event.

internal prelude (via an implicit set substitute file)

In Ruby, "require_relative" is a method which is defined in a C string which is essentially passed to "eval". In the debugger though, we put the lines of that string into a file, and then tell the debugger to use that whenever it wants to list the source for that method.

  $ trepan /tmp/rr.rb
  -- (/tmp/rr.rb:1)
  require_relative 'ev'
  (trepan): step
  -> (<internal:prelude>:28 remapped /src/external-vcs/trepan/data/prelude.rb:2)
  def require_relative(relative_feature)
  (trepan): list
   23         end
   24       end
   26       module Kernel
   27         module_function
   28  ->     def require_relative(relative_feature)
   29           c = caller.first
   30           e = c.rindex(/:\d+:in /)
   31           file = $`
   32           if /\A\((.*)\)/ =~ file # eval, etc.

set string substitute

More generally, if you have a variable that gets eval'd, there is a way to tell the debugger to create a file from that string and use this temporary file for reference for listing:

  $ trepan /tmp/ev.rb
  -- (/tmp/ev.rb:1)
  x = '
  (trepan): list
    1  ->    x = '
    2        class Foo
    3          def bar
    4             5
    5          end
    6        end
    7        '
    8        eval(x)
    9        f =
  (trepan): c 10
  xx (/tmp/ev.rb:10)
  (trepan): set substitute string (eval) x
  (trepan): s
  -- (/tmp/ev.rb:10)
  (trepan): s
  .. (/tmp/(eval)-x-20091023-7525-tqcd7l.rb:3)
  def bar
  (trepan): list
    2       class Foo
    3         def bar
    4            5
    5         end
    6       end
  (trepan): where
  --> #0 METHOD Foo#bar() in string (eval)
      #1 TOP Object#<top /tmp/ev.rb> file /tmp/ev.rb at line 10

Note: there may ultimately be lots of strings called "(eval)" so distinguishing between them is a problem. Eventually we will address this..

The instruction sequence name "" is due to a patch to Ruby 1.9. In an unpatched Ruby 1.9 the name would be the unhelpful and cryptic "". Adding the file name in the instruction sequence name also allows us to disambiguate between several source files compiled would would otherwise have the same name.

In cases where the debugger the evaluated string is still on the call stack, the debugger will detect that and automatically do a string-to-file substition:

 $ trepan /tmp/ev.rb
  -- (/tmp/ev.rb:1)
  x = '
  (trepan): step to eval
  CFUNC Kernel#eval("\nclass "...)
  C> (/tmp/ev.rb:8)   
  (trepan): s+
  -- (/tmp/eval-20100331-17221-12r49n9.rb:2)  # sees eval string on call stack here.
  class Foo
  (trepan): list
    2   class Foo
    3     def bar
    4       5
    5     end
    6   end


VM hacking

You can use the debugger to understand more at the VM-level what's going on. The need for this is probably not of interest to most Ruby programmers. However if you are interested in hacking the VM say to find a bug in code generation, understand the performance issues, or figure out how to improve generated code, the debugger may be able to help.

When a breakpoint is set, we report exactly where in the VM instruction sequence you are stopping at:

  $ trepan /tmp/gcd.rb 3 5
  -- (/tmp/gcd.rb:4)
  def gcd(a, b)
  (trepan): list
    1       #!/usr/bin/env ruby
    3       # GCD. We assume positive numbers
    4  ->   def gcd(a, b)
    5         # Make: a <= b
    6         if a > b
    7           a, b = [b, a]
    8         end
   10         return nil if a <= 0
  (trepan): b 6
  Breakpoint 1 set in file /tmp/gcd.rb,
    VM offset 4 of instruction sequence gcd.
  (trepan): c
  xx (/tmp/gcd.rb:6)
  if a > b
  (trepan): s+
  -- (/tmp/gcd.rb:6)
  if a > b
  (trepan): s+
  .. (/tmp/gcd.rb:6)
  if a > b
(trepan): disassemble
  == disasm: <RubyVM::InstructionSequence:gcd@/tmp/gcd.rb>
  local table (size: 3, argc: 2 [opts: 0, rest: -1, post: 0, block: -1] s1)
  [ 3] a<Arg>     [ 2] b<Arg>     
      0000 trace            8                                            (   4)
      0002 trace            1                                            (   6)
B     0004 getlocal         a
      0006 getlocal         b
  --> 0008 opt_gt           <ic>
      0010 branchunless     22
      0012 trace            1                                            (   7)
      0014 getlocal         b
      0016 getlocal         a
      0018 setlocal         b
      0020 setlocal         a
      0022 trace            1                                            (  10)
      0024 getlocal         a
      0026 putobject        0
      0028 opt_le           <ic>
      0030 branchunless     38
      0032 jump             34
      0034 putnil           
      0035 trace            16
      0037 leave            
      0038 trace            1                                            (  12)
      0040 getlocal         a
      0042 putobject        1
      0044 opt_eq           <ic>
      0046 branchif         60
      0048 getlocal         b
      0050 getlocal         a
      0052 opt_minus        <ic>
      0054 putobject        0
      0056 opt_eq           <ic>

One can inspect VM stack locations and registers:

  (trepan): info reg sp 1
  VM sp(1) = 5
  (trepan): info reg sp 2
  VM sp(2) = 3
  (trepan): info reg lfp 0
  VM lfp(0) = 3
  (trepan): info reg lfp 1
  VM lfp(1) = 5
  (trepan): info reg pc
  VM pc = 8

Finally, we allow you to specify a VM instruction to stop at. Below we will "continue" to offset 52. Offset are represented by prefacing a number with 'o';

  (trepan): c o52
  xx (/tmp/gcd.rb:12)
  if a == 1 or b-a == 0
  (trepan): info reg pc
  VM pc = 52

Debugger hacking

One of the sad things about working on a debugger, is that the debugger writer is often handicapped in his/her ability to use this nice tool that is being provided to everyone else.

By making the debugger modular, Ruby's dynamic behvior comes to the rescue!

Below I show fixing a bug in the "set event" command:

  $ trepan /tmp/gcd.rb 3 5
  -- (/tmp/gcd.rb:4)
  def gcd(a, b)
  (trepan): where
  --> #0 TOP Object#<top /tmp/ev.rb> in file /tmp/gcd.rb at line 4

A little bit of a digression here. The above stack listing is a bit of a fake. Recall I issued trepan from the outset. The debugger is in fact hiding the upper levels from causal perusal. However if you really want the down-and-dirty details, that's available:

  (trepan): set debug stack on
   debugstack is on.
  (trepan): where
--> #0 TOP Object#<top /tmp/ev.rb> in file /tmp/gcd.rb at line 4
    #1 CFUNC Module#load() in file /src/external-vcs/trepan/lib/run.rb at line 39
    #2 BLOCK Object#block in debug_program in file /src/external-vcs/trepan/lib/run.rb at line 39
    #3 CFUNC Proc#call() in file /src/external-vcs/trepan/trepan.rb at line 68

Okay now to the little bug I had. The bug is that in the "set events" command the word "events" was treated as an event name like "c_call" rather than a word that needed to be skipped over.

  (trepan): set events c_call, c_return
  *** Event names unrecognized/ignored: events

Magic happens now while I fix the code in file "set_subcmd/events.rb". I then reload that file, and presto!

  (trepan): load '/src/external-vcs/trepan/processor/command/set_subcmd/events.rb'
  (trepan): set event c_call, c_return
  Trace events we may stop on:
    c_call, c_return

Finally, there is complete access to the debugger innerds. The "-d" option on irb has a global variable $trepan_frame set to a RubyVM::ThreadFrame for the point where the debugged program has stopped.

  (trepan): irb -d
  You are in a trepan session. You should have access to program scope.
  'dbgr', 'step', 'n', 'q', 'cont' commands have been added.
  You should have access to debugger state via global variable $trepan
  You should have access to the program frame via global variable $trepan_frame
  You should have access to the command processor via global variable $trepan_cmdproc
  trepan >> $trepan.settings
  => {:cmdproc_opts=>{}, :core_opts=>{}, :restart_argv=>["/usr/local/bin/ruby", "/src/external-vcs/trepan/bin/trepan", "./tmp/gcd.rb", "3", "5"]}
  trepan >> $trepan.core.settings
  => {:cmdproc_opts=>{}, :hook_name=>:event_processor, :step_count=>0, :async_events=>196736, :step_events=>927}
  trepan >> $trepan.core.processor.settings
  => {:autoeval=>true, :autoirb=>false, :autolist=>false, :basename=>false, :different=>true, :debugexcept=>true, :debugskip=>false, :debugstack=>true, :listsize=>10, :maxstring=>150, :prompt=>"(trepan): ", :width=>80}
  trepan >> fr = $trepan.core.processor.frame
  => #<RubyVM::ThreadFrame:0x00000001e008e8>