Skip to content

Instantly share code, notes, and snippets.

@csfrancis
Last active May 29, 2023 05:31
Show Gist options
  • Star 66 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • Save csfrancis/11376304 to your computer and use it in GitHub Desktop.
Save csfrancis/11376304 to your computer and use it in GitHub Desktop.
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
Copy link
Author

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

@csfrancis
Copy link
Author

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
Copy link

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
Copy link

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
Copy link
Author

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
Copy link

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
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
Copy link

@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