Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Dump an MRI call stack from gdb
# Updated for Ruby 2.3
string_t = None
def get_rstring(addr):
s = addr.cast(string_t.pointer())
if s['basic']['flags'] & (1 << 13):
return s['as']['heap']['ptr'].string()
else:
return s['as']['ary'].string()
def get_lineno(iseq, pos):
if pos != 0:
pos -= 1
t = iseq['line_info_table']
t_size = iseq['line_info_size']
if t_size == 0:
return 0
elif t_size == 1:
return t[0]['line_no']
for i in range(0, int(t_size)):
if pos == t[i]['position']:
return t[i]['line_no']
elif t[i]['position'] > pos:
return t[i-1]['line_no']
return t[t_size-1]['line_no']
def get_ruby_stacktrace(th=None):
global string_t
try:
control_frame_t = gdb.lookup_type('rb_control_frame_t')
string_t = gdb.lookup_type('struct RString')
except gdb.error:
raise gdb.GdbError ("ruby extension requires symbols")
if th == None:
th = gdb.parse_and_eval('ruby_current_thread')
else:
th = gdb.parse_and_eval('(rb_thread_t *) %s' % th)
last_cfp = th['cfp']
start_cfp = (th['stack'] + th['stack_size']).cast(control_frame_t.pointer()) - 2
size = start_cfp - last_cfp + 1
cfp = start_cfp
call_stack = []
for i in range(0, int(size)):
if cfp['iseq'].dereference().address != 0:
if cfp['pc'].dereference().address != 0:
s = "{}:{}:in `{}'".format(get_rstring(cfp['iseq']['body']['location']['path']),
get_lineno(cfp['iseq']['body'], cfp['pc'] - cfp['iseq']['body']['iseq_encoded']),
get_rstring(cfp['iseq']['body']['location']['label']))
call_stack.append(s)
cfp -= 1
for i in reversed(call_stack):
print(i)
end
@csfrancis

This comment has been minimized.

Copy link
Owner Author

csfrancis commented Apr 28, 2014

Requires a ruby that is built with symbols. To run from gdb, you can run python then paste the script.

@csfrancis

This comment has been minimized.

Copy link
Owner Author

csfrancis commented Apr 28, 2014

For example:

(gdb) python
>def get_rstring(addr):
>  s = addr.cast(string_t.pointer())
>  if s['basic']['flags'] & (1 << 13):
>    return s['as']['heap']['ptr'].string()
>  else:
>    return s['as']['ary'].string()
>
>def get_lineno(iseq, pos):
>  if pos != 0:
>    pos -= 1
>  t = iseq['line_info_table']
>  t_size = iseq['line_info_size']
>
>  if t_size == 0:
>    return 0
>  elif t_size == 1:
>    return t[0]['line_no']
>
>  for i in range(0, t_size):
>    if pos == t[i]['position']:
>      return t[i]['line_no']
>    elif t[i]['position'] > pos:
>      return t[i-1]['line_no']
>
>  return t[t_size-1]['line_no']
>
>try:
>  control_frame_t = gdb.lookup_type('rb_control_frame_t')
>  string_t = gdb.lookup_type('struct RString')
>except gdb.error:
>  raise gdb.GdbError ("ruby extension requires symbols")
>
>th = gdb.parse_and_eval('ruby_current_thread')
>
>last_cfp = th['cfp']
>start_cfp = (th['stack'] + th['stack_size']).cast(control_frame_t.pointer()) - 2
>size = start_cfp - last_cfp + 1
>cfp = start_cfp
>call_stack = []
>for i in range(0, size):
>  if cfp['iseq'].dereference().address != 0:
>    if cfp['pc'].dereference().address != 0:
>      s = "{}:{}:in `{}'".format(get_rstring(cfp['iseq']['location']['path']),
>        get_lineno(cfp['iseq'], cfp['pc'] - cfp['iseq']['iseq_encoded']),
>        get_rstring(cfp['iseq']['location']['label']))
>      call_stack.append(s)
>
>  cfp -= 1
>
>for i in reversed(call_stack):
>  print i
>
>end
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/input-method.rb:152:in `gets'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:472:in `block (2 levels) in eval_input'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:624:in `signal_status'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:471:in `block in eval_input'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:190:in `buf_input'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:105:in `getc'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/slex.rb:206:in `match_io'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/slex.rb:76:in `match'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:290:in `token'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:266:in `lex'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:237:in `block (2 levels) in each_top_level_statement'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:233:in `block in each_top_level_statement'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb/ruby-lex.rb:232:in `each_top_level_statement'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:488:in `eval_input'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:397:in `block in start'
/usr/lib/shopify-ruby/2.1.1-shopify2/lib/ruby/2.1.0/irb.rb:396:in `start'
/usr/lib/shopify-ruby/2.1.1-shopify2/bin/irb:11:in `<main>'
(gdb)
@sirupsen

This comment has been minimized.

Copy link

sirupsen commented Jul 28, 2014

From @charliesome:

    local BT_FILENAME="/tmp/ruby-backtrace-$$.txt"
    sudo gdb "$RUBY_BINARY" "$PID" -batch \
        -ex "$THREAD" \
        -ex 'p (int)dup(1)' \
        -ex 'p (int)dup(2)' \
        -ex 'p (int)creat("'"$BT_FILENAME"'", 0644)' \
        -ex 'p (int)dup2($3, 1)' \
        -ex 'p (int)dup2($3, 2)' \
        -ex 'call (void)rb_backtrace()' \
        -ex 'p (int)dup2($1, 1)' \
        -ex 'p (int)dup2($2, 2)' \
        -ex 'p (int)close($1)' \
        -ex 'p (int)close($2)' \
        -ex 'p (int)close($3)' \
        &>/dev/null
    cat "$BT_FILENAME"
    rm "$BT_FILENAME"

(would not work on a coredump)

@dylanahsmith

This comment has been minimized.

Copy link

dylanahsmith commented Jan 7, 2015

The above script just prints the current thread. This gdb script will a ruby backtrace for all ruby threads to /tmp/ruby-backtrace.txt

set $old_stdout = dup(1)
set $old_stderr = dup(2)
set $fd = creat("/tmp/ruby-backtrace.txt", 0644)
call dup2($fd, 1)
call dup2($fd, 2)
set $thread_list = rb_thread_list()
set $num_threads = rb_num2long(rb_ary_length($thread_list))
set $i = 0
while $i < $num_threads
  call rb_p(rb_thread_backtrace_m(0, 0, rb_ary_entry($thread_list, $i++)))
end
call dup2($old_stdout, 1)
call dup2($old_stderr, 2)
call close($old_stdout)
call close($old_stderr)
call close($fd)
@csfrancis

This comment has been minimized.

Copy link
Owner Author

csfrancis commented Feb 9, 2016

This snippet now supports multiple threads.

python get_ruby_stacktrace()

By default this will return the stack from the main thread.

python get_ruby_stacktrace("0x7fed6cd81800")

This would return a stack by casting 0x7fed6cd81800 to an rb_thread_t. How do you determine the address of the rb_thread_t? You can find it in the C call stack for the given thread:

(gdb) info threads
  Id   Target Id         Frame
  5    Thread 0x7fed64ca6700 (LWP 32573) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  4    Thread 0x7fed5f9a7700 (LWP 32574) "ruby" pthread_cond_timedwait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S:238
  3    Thread 0x7fed5f8a6700 (LWP 32575) "ruby" pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
  2    Thread 0x7fed64cbc700 (LWP 32577) "ruby-timer-thr" 0x00007fed63c1912d in poll () at ../sysdeps/unix/syscall-template.S:81
* 1    Thread 0x7fed64ca8780 (LWP 32531) "ruby" 0x00007fed63c1dda3 in select () at ../sysdeps/unix/syscall-template.S:81
(gdb) thread 3
[Switching to thread 3 (Thread 0x7fed5f8a6700 (LWP 32575))]
#0  pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
185     ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S: No such file or directory.
(gdb) where
#0  pthread_cond_wait@@GLIBC_2.3.2 () at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
#1  0x00007fed64ec7093 in native_cond_wait (mutex=0x7fed6c8fcd80, cond=0x7fed6c8fcda8) at thread_pthread.c:334
#2  lock_func (timeout_ms=0, mutex=0x7fed6c8fcd80, th=0x7fed6cd81800) at thread.c:4343
#3  rb_mutex_lock (self=140657681410960) at thread.c:4417
#4  0x00007fed64ea3a65 in vm_call_cfunc_with_frame (th=0x7fed6cd81800, reg_cfp=0x7fed6ce85d30, ci=<optimized out>) at vm_insnhelper.c:1510
#5  0x00007fed64eaa9b4 in vm_exec_core (th=th@entry=0x7fed6cd81800, initial=initial@entry=0) at insns.def:1028
#6  0x00007fed64eaf1ed in vm_exec (th=th@entry=0x7fed6cd81800) at vm.c:1398
#7  0x00007fed64ea4c4a in invoke_block_from_c (th=th@entry=0x7fed6cd81800, block=block@entry=0x7fed6cc480d0, self=self@entry=140657682652720, argc=argc@entry=0, argv=argv@entry=0x7fed6b35f7a0, blockptr=blockptr@entry=0x0,
    cref=cref@entry=0x0, defined_class=defined_class@entry=140657681351360) at vm.c:817
#8  0x00007fed64ea57bb in vm_invoke_proc (th=th@entry=0x7fed6cd81800, proc=proc@entry=0x7fed6cc480d0, self=140657682652720, defined_class=140657681351360, argc=0, argv=0x7fed6b35f7a0, blockptr=blockptr@entry=0x0) at vm.c:881
#9  0x00007fed64ea586a in rb_vm_invoke_proc (th=th@entry=0x7fed6cd81800, proc=proc@entry=0x7fed6cc480d0, argc=<optimized out>, argv=<optimized out>, blockptr=blockptr@entry=0x0) at vm.c:900
#10 0x00007fed64ec950a in thread_start_func_2 (th=th@entry=0x7fed6cd81800, stack_start=<optimized out>) at thread.c:535
#11 0x00007fed64ec991b in thread_start_func_1 (th_ptr=0x7fed6cd81800) at thread_pthread.c:840 <-- RIGHT HERE
#12 0x00007fed6463c182 in start_thread (arg=0x7fed5f8a6700) at pthread_create.c:312
#13 0x00007fed63c2647d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

You can find the rb_thread_t pointer passed as the parameter to thread_start_func_1.

@andrewdavidmackenzie

This comment has been minimized.

Copy link

andrewdavidmackenzie commented Aug 15, 2016

If placed in your gdb data-directory, I assume it will then be automatically found by gdb and can be called - and no need to paste into gdb?

@thegedge

This comment has been minimized.

Copy link

thegedge commented Nov 1, 2016

If you're in ruby 2.3 you'll need to replace all instances of cfp['iseq'] with cfp['iseq']['body'] (except for the dereference null check).

@sribalakumar

This comment has been minimized.

Copy link

sribalakumar commented Mar 7, 2019

@csfrancis On a core dump from a ruby process, is it possible to get the Thread.current[:foo] values ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.