gdbruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d8f60d770afee00f6dcff8f144788e63210a5714
4
+ data.tar.gz: 08a8449dc823f8d43403bf09ceca37499e37619f
5
+ SHA512:
6
+ metadata.gz: 57c4b47fd4b823cbfcf7461eb5561f9f36b68da821c0c0cad394123514a4cf2ccdf2c18863eea8a3d02e8f1d336646acb78561678bd3d8b7c8762028c32e34bc
7
+ data.tar.gz: 6b62783d5847fa6f787bba87739599ec33456d4bce35f1e8eb8eba617f0d47ddcd7e561ebb2dff374fb9ad4851b3a782e005e7719667c1a4e89f0ff7f9f3c058
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .ruby-version
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - "2.0.0"
5
+
6
+ before_install:
7
+ - sudo apt-get -qq update
8
+ - sudo apt-get -qq install gdb
9
+
10
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,68 @@
1
+ # gdbruby.rb
2
+
3
+ ## Overview
4
+
5
+ gdbruby.rb can output these information with live process or core.
6
+
7
+ - environment variables
8
+ - C stacktrace
9
+ - Ruby backtrace
10
+
11
+ This is Ruby port of gdbperl.pl made by Akira Higuchi.
12
+
13
+ ## Precondition
14
+
15
+ Your Ruby executable must have debug symbol.
16
+
17
+ ## Usage
18
+
19
+ With live process(process id: 24113)
20
+
21
+ ```sh
22
+ $ gdbruby.rb 24113
23
+ ```
24
+
25
+ With core file. You have to specify path of ruby executable.
26
+
27
+ ```sh
28
+ $ gdbruby.rb core.24113 `rbenv which ruby`
29
+ ```
30
+
31
+ You can get core file with gcore script or execute gcore command on gdb like below.
32
+
33
+ ```
34
+ $ gdb
35
+ (gdb) attach 24113
36
+ (gdb) gcore core.24113
37
+ (gdb) detach
38
+ ```
39
+
40
+ ## Options
41
+
42
+ You can specify options. 0 is interprited as false.
43
+
44
+ ```sh
45
+ $ gdbruby.rb 24113 verbose_gdb=1 c_trace=1
46
+ ```
47
+
48
+ - verbose_gdb: Show request and response to/from gdb(default: false)
49
+ - env: Show environment variables(default: true)
50
+ - c_trace: Show C stacktrace(default:true)
51
+
52
+ ## ToDo
53
+
54
+ - Print all Ruby threads
55
+ - Print arguments on Ruby backtrace
56
+ - Speeding up Ruby's type check
57
+ - List Ruby objects
58
+ - Check memory usage
59
+
60
+ ## FAQ
61
+
62
+ ### Why don't you call functions such like rb_vm_get_sourceline()
63
+
64
+ If you use gdbruby.rb with live process, gdb can call these functions. But if you use gdbruby.rb with core file, gdb cannot call c functions. So I re-implement these functions.
65
+
66
+ ### Which version does it support?
67
+
68
+ Ruby 2.0.0 only.
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+ task :default => :spec
10
+
11
+ desc 'Open an irb session preloaded with the gem library'
12
+ task :console do
13
+ sh 'irb -rubygems -I lib'
14
+ end
15
+ task :c => :console
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,533 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set expandtab ts=2 sw=2 nowrap ft=ruby ff=unix : */
3
+
4
+ # gdbruby.rb - shows the call trace of a running ruby process
5
+ #
6
+ # Ruby porting of gdbperl.pl.
7
+ # gdbperl.pl is made by ahiguchi.
8
+ # https://github.com/ahiguti/gdbperl
9
+ #
10
+ # Copyright (c) Tasuku SUENAGA a.k.a. gunyarakun
11
+ # All rights reserved.
12
+ #
13
+ # Redistribution and use in source and binary forms, with or without
14
+ # modification, are permitted provided that the following conditions are met:
15
+ #
16
+ # Redistributions of source code must retain the above copyright notice,
17
+ # this list of conditions and the following disclaimer.
18
+ # Redistributions in binary form must reproduce the above copyright notice,
19
+ # this list of conditions and the following disclaimer in the
20
+ # documentation and/or other materials provided with the distribution.
21
+ # Neither the name of the author nor the names of its contributors
22
+ # may be used to endorse or promote products derived from this software
23
+ # without specific prior written permission.
24
+ #
25
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
26
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
29
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
30
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
31
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35
+ # POSSIBILITY OF SUCH DAMAGE.
36
+ #
37
+ # Usage: gdbruby.rb PROCESS_ID [ruby_EXECUTABLE] [OPTION=VALUE [...]]
38
+ # gdbruby.rb CORE_FILE ruby_EXECUTABLE [OPTION=VALUE [...]]
39
+
40
+ require 'open3'
41
+
42
+ class GDBRubyConfig
43
+ attr_reader :core_or_pid, :exe, :is_pid
44
+
45
+ def initialize(argvs)
46
+ @config_map = {}
47
+ @argv = []
48
+
49
+ argvs.each do |argv|
50
+ if argv =~ /^(\w+)=(.+)$/
51
+ @config_map[$1] = $2
52
+ else
53
+ @argv << argv
54
+ end
55
+ end
56
+
57
+ parse_argv
58
+ end
59
+
60
+ def parse_argv
61
+ @core_or_pid = @argv[0]
62
+
63
+ unless @core_or_pid
64
+ message =
65
+ "Usage: #{$0} PROCESS_ID [ruby_EXECUTABLE] [OPTION=VALUE [...]]\n" +
66
+ "Usage: #{$0} CORE_FILE ruby_EXECUTABLE [OPTION=VALUE [...]]\n"
67
+ puts message
68
+ exit 1
69
+ end
70
+
71
+ exe = @argv[1]
72
+
73
+ @is_pid = (@core_or_pid =~ /^\d+$/)
74
+ if @is_pid
75
+ if exe.nil?
76
+ begin
77
+ if RUBY_PLATFORM =~ /linux/
78
+ exe = File.readlink("/proc/#{@core_or_pid}/exe")
79
+ end
80
+ rescue
81
+ end
82
+ end
83
+
84
+ if exe.nil?
85
+ exe = `rbenv which ruby`
86
+ exe = `which ruby` unless FileTest.exist?(exe)
87
+ exe.chomp!
88
+ end
89
+ end
90
+
91
+ raise "failed to detect ruby executable" unless exe
92
+ @exe = exe
93
+ end
94
+
95
+ def [](key, default_value = nil)
96
+ if @config_map.has_key?(key)
97
+ return case default_value
98
+ when TrueClass, FalseClass
99
+ not (@config_map[key].empty? || @config_map[key] == '0')
100
+ when Numeric
101
+ @config_map[key].to_i
102
+ else
103
+ @config_map[key]
104
+ end
105
+ end
106
+ default_value
107
+ end
108
+ end
109
+
110
+ class GDB
111
+ COMMAND_READ_BUFFER_SIZE = 1024
112
+ attr_reader :exec_options
113
+
114
+ def initialize(config)
115
+ @config = config
116
+ @exec_options = ['gdb', '-silent', '-nw', @config.exe, @config.core_or_pid]
117
+ end
118
+
119
+ def run
120
+ @gdb_stdin, @gdb_stdout, @gdb_stderr = *Open3.popen3(*@exec_options)
121
+ prepare
122
+ begin
123
+ yield
124
+ detach
125
+ ensure
126
+ if @config.is_pid
127
+ Process.kill('CONT', @config.core_or_pid.to_i)
128
+ end
129
+ @gdb_stdin.close
130
+ @gdb_stdout.close
131
+ @gdb_stderr.close
132
+ end
133
+ end
134
+
135
+ def prepare
136
+ cmd_exec('')
137
+ cmd_exec('set pagination off')
138
+ end
139
+
140
+ def detach
141
+ cmd_get_value("detach")
142
+ cmd_get_value("quit")
143
+ end
144
+
145
+ def log_gdb(pre, message)
146
+ return unless @config['verbose_gdb', false]
147
+ message.each_line do |line|
148
+ puts "#{pre}: #{line}"
149
+ end
150
+ end
151
+
152
+ def cmd_get_pointer(cmd, type)
153
+ response = cmd_exec(cmd)
154
+ raise "Invalid pointer #{response}" unless response =~ /\(#{type} \*\) (0x[0-9a-f]+)/
155
+ $1
156
+ end
157
+
158
+ def cmd_exec(cmd)
159
+ log_gdb('C', cmd)
160
+ if cmd
161
+ send_cmd = cmd.empty? ? cmd : "#{cmd}\n"
162
+ r = @gdb_stdin.syswrite(send_cmd)
163
+ if r < send_cmd.length
164
+ raise "failed to send: [#{cmd}]"
165
+ end
166
+ end
167
+
168
+ responses = []
169
+ while true
170
+ # TODO: specify buffer size
171
+ begin
172
+ buf = @gdb_stdout.sysread(COMMAND_READ_BUFFER_SIZE)
173
+ rescue
174
+ break
175
+ end
176
+ responses << buf
177
+ break if buf =~ /\(gdb\) $/
178
+ end
179
+
180
+ response = responses.join('')
181
+ log_gdb('R', response)
182
+ response
183
+ end
184
+
185
+ def cmd_get_value(cmd)
186
+ response = cmd_exec(cmd)
187
+ return '' unless response =~ /\A\$\d+ =\s+(.+)/
188
+
189
+ value = $1
190
+ if value =~ /0x\w+\s+\"(.+)\"/
191
+ $1
192
+ else
193
+ value
194
+ end
195
+ end
196
+ end
197
+
198
+ class RubyInternal
199
+ FL_USHIFT = 12
200
+ FL_USER1 = 1 << (FL_USHIFT + 1)
201
+ RSTRING_NOEMBED = FL_USER1
202
+
203
+ VM_FRAME_MAGIC_CFUNC = 0x61
204
+ VM_FRAME_MAGIC_MASK_BITS = 8
205
+ VM_FRAME_MAGIC_MASK = ~(~0 << VM_FRAME_MAGIC_MASK_BITS)
206
+
207
+ def initialize(gdb)
208
+ @gdb = gdb
209
+ end
210
+
211
+ def ruby_vm_ifunc_p(pointer)
212
+ # pointer is string like 0xaabbccdd
213
+ @gdb.cmd_get_value("p (enum ruby_value_type)(((struct RBasic *)(#{pointer}))->flags & RUBY_T_MASK) == RUBY_T_NODE") != '0'
214
+ end
215
+
216
+ def ruby_vm_normal_iseq_p(pointer)
217
+ @gdb.cmd_get_value("p #{pointer} && #{pointer} != 0") != '0' and not ruby_vm_ifunc_p(pointer)
218
+ end
219
+
220
+ def rb_vm_get_sourceline(cfp, iseq)
221
+ if ruby_vm_normal_iseq_p(iseq)
222
+ # calc_lineno()@vm_backtrace.c
223
+ current_position = @gdb.cmd_get_value("p #{cfp}->pc - #{iseq}->iseq_encoded").to_i
224
+ # rb_iseq_line_no()@iseq.c
225
+ current_position -= 1 unless current_position == 0
226
+ # find_line_no@iseq.c and get_line_info@iseq.c
227
+ line_info_size = @gdb.cmd_get_value("p #{iseq}->line_info_size").to_i
228
+ line_info_table = "#{iseq}->line_info_table"
229
+ case line_info_size
230
+ when 0
231
+ return 0
232
+ when 1
233
+ return @gdb.cmd_get_value("p #{line_info_table}[0].line_no").to_i
234
+ else
235
+ (1..line_info_size).each do |i|
236
+ position = @gdb.cmd_get_value("p #{line_info_table}[#{i}].position").to_i
237
+ if position == current_position
238
+ return @gdb.cmd_get_value("p #{line_info_table}[#{i}].line_no").to_i
239
+ elsif position > current_position
240
+ return @gdb.cmd_get_value("p #{line_info_table}[#{i - 1}].line_no").to_i
241
+ end
242
+ end
243
+ end
244
+ end
245
+ 0
246
+ end
247
+
248
+ # NOTE: This logic is slow because many commands are sent to gdb.
249
+ # Fetch consts with 'ptype enum ruby_value_type' first and
250
+ # check types in Ruby.
251
+ def rb_type(value)
252
+ type_str = nil
253
+ # IMMEDIATE_P
254
+ if @gdb.cmd_get_value("p (int)(#{value}) & RUBY_IMMEDIATE_MASK") != '0'
255
+ # FIXNUM_P
256
+ if @gdb.cmd_get_value("p (int)(#{value}) & RUBY_FIXNUM_FLAG") != '0'
257
+ type_str = 'RUBY_T_FIXNUM'
258
+ # FLONUM_P
259
+ elsif @gdb.cmd_get_value("p ((int)(#{value}) & RUBY_FLONUM_MASK) == RUBY_FLONUM_FLAG") != '0'
260
+ type_str = 'RUBY_T_FLONUM'
261
+ elsif @gdb.cmd_get_value("p (#{value}) == RUBY_Qtrue") != '0'
262
+ type_str = 'RUBY_T_TRUE'
263
+ # SYMBOL_P
264
+ elsif @gdb.cmd_get_value("p (VALUE)(#{value}) & ~(~(VALUE)0 << RUBY_SPECIAL_SHIFT) == RUBY_SYMBOL_FLAG") != '0'
265
+ type_str = 'RUBY_T_SYMBOL'
266
+ elsif @gdb.cmd_get_value("p (#{value}) == RUBY_Qundef") != '0'
267
+ type_str = 'RUBY_T_UNDEF'
268
+ end
269
+ elsif @gdb.cmd_get_value("p (int)(#{value}) & RUBY_FIXNUM_FLAG") != '0'
270
+ # special consts
271
+ const = @gdb.cmd_get_value("p (enum ruby_special_consts)(#{value})")
272
+ # TODO: change to map
273
+ case const
274
+ when 'RUBY_Qnil'
275
+ type_str = 'RUBY_T_NIL'
276
+ when 'RUBY_Qfalse'
277
+ type_str = 'RUBY_T_FALSE'
278
+ end
279
+ else
280
+ # builtin type
281
+ type_str = @gdb.cmd_get_value("p (enum ruby_value_type)(((struct RBasic*)(#{value}))->flags & RUBY_T_MASK)")
282
+ end
283
+ end
284
+
285
+ def rstring_ptr(value_pointer)
286
+ no_embed = @gdb.cmd_get_value("p ((struct RBasic *)(#{value_pointer}))->flags & #{RSTRING_NOEMBED}")
287
+ if no_embed == '0'
288
+ # embedded in struct
289
+ @gdb.cmd_get_value("p (char *)((struct RString *)(#{value_pointer}))->as.ary")
290
+ else
291
+ # heap pointer
292
+ @gdb.cmd_get_value("p (char *)((struct RString *)(#{value_pointer}))->as.heap.ptr")
293
+ end
294
+ end
295
+
296
+ def rubyvm_cfunc_frame_p(cfp)
297
+ @gdb.cmd_get_value("p (#{cfp}->flag & #{VM_FRAME_MAGIC_MASK}) == #{VM_FRAME_MAGIC_CFUNC}") != '0'
298
+ end
299
+
300
+ def do_hash(key, table)
301
+ # NOTE: table->type->hash is always st_numhash
302
+ key
303
+ end
304
+
305
+ def st_lookup(table, key)
306
+ hash_val = do_hash(key, table)
307
+
308
+ raise if @gdb.cmd_get_value("p (#{table})->entries_packed") != '0'
309
+ raise if @gdb.cmd_get_value("p (#{table})->type->hash == st_numhash") == '0'
310
+ raise if @gdb.cmd_get_value("p (#{table})->type->compare == st_numcmp") == '0'
311
+
312
+ # TODO: check table->entries_packed
313
+ bin_pos = @gdb.cmd_get_value("p (#{hash_val}) % (#{table})->num_bins")
314
+
315
+ ptr = find_entry(table, key, hash_val, bin_pos)
316
+
317
+ if ptr.hex == 0
318
+ nil
319
+ else
320
+ value = @gdb.cmd_get_value("p ((struct st_table_entry *)(#{ptr}))->record")
321
+ value
322
+ end
323
+ end
324
+
325
+ def ptr_not_equal(table, ptr, hash_val, key)
326
+ ptr =~ /(0x[0-9a-f]+)\z/
327
+ ptr_num = $1.hex
328
+ t_hash = @gdb.cmd_get_value("p (#{ptr})->hash")
329
+ t_key = @gdb.cmd_get_value("p (#{ptr})->key")
330
+ # NOTE: table->type->compare is always st_numcmp
331
+ ptr_num != 0 and (t_hash != hash_val or t_key != key)
332
+ end
333
+
334
+ def find_entry(table, key, hash_val, bin_pos)
335
+ ptr = @gdb.cmd_get_value("p (#{table})->as.big.bins[#{bin_pos}]")
336
+ if ptr_not_equal(table, ptr, hash_val, key)
337
+ next_ptr = @gdb.cmd_get_value("p (#{ptr})->next")
338
+ while ptr_not_equal(table, next_ptr, hash_val, key)
339
+ ptr = next_ptr
340
+ next_ptr = @gdb.cmd_get_value("p (#{ptr})->next")
341
+ end
342
+ ptr = next_ptr
343
+ end
344
+ ptr =~ /(0x[0-9a-f]+)\z/
345
+ $1
346
+ end
347
+
348
+ def rb_id2str(id)
349
+ rstring_ptr(st_lookup('global_symbols.id_str', id))
350
+ end
351
+ end
352
+
353
+ class GDBRuby
354
+ MAX_FRAMES = 30
355
+
356
+ def initialize(config)
357
+ @config = config
358
+ end
359
+
360
+ def trace
361
+ @gdb = GDB.new(@config)
362
+ @ri = RubyInternal.new(@gdb)
363
+ puts "command:\n#{@gdb.exec_options.join(' ')}"
364
+ puts ''
365
+ @gdb.run do
366
+ show_environ if @config['env', true]
367
+ show_ruby_version
368
+ show_backtrace
369
+ end
370
+ end
371
+
372
+ def show_environ
373
+ i = 0
374
+ puts "environ:"
375
+ while true
376
+ response = @gdb.cmd_get_value("p ((char **)environ)[#{i}]")
377
+ break if response.empty? or response == '0x0'
378
+ puts response
379
+ i += 1
380
+ end
381
+ puts ''
382
+ end
383
+
384
+ def get_map_of_ruby_thread_pointer_to_gdb_thread_id
385
+ # After 'set pagination off' is executed,
386
+ # thread info is one line per thread.
387
+ map = {}
388
+ @gdb.cmd_exec('info threads').each_line do |line|
389
+ if line =~ /\A[\s\*]+(\d+)/
390
+ gdb_thread_id = $1
391
+ @gdb.cmd_exec("thread #{gdb_thread_id}")
392
+ @gdb.cmd_exec("backtrace").each_line do |bt_line|
393
+ if bt_line =~ /\(th=(0x[0-9a-f]+)/
394
+ ruby_thread_pointer = $1
395
+ map[ruby_thread_pointer] = gdb_thread_id
396
+ break
397
+ end
398
+ end
399
+ end
400
+ end
401
+ map
402
+ end
403
+
404
+ def get_ruby_frame(frame_count)
405
+ # Accessor
406
+ cfp = "(ruby_current_thread->cfp + #{frame_count})"
407
+ iseq = "#{cfp}->iseq"
408
+
409
+ # Check iseq
410
+ iseq_ptr = @gdb.cmd_get_pointer("p #{iseq}", 'rb_iseq_t')
411
+ if iseq_ptr.hex != 0
412
+ # TODO: check cfp->pc is null or not
413
+
414
+ iseq_type = @gdb.cmd_get_value("p #{iseq}->type").intern
415
+
416
+ case iseq_type
417
+ when :ISEQ_TYPE_TOP
418
+ return
419
+ end
420
+
421
+ # Ruby function
422
+ @prev_location = {
423
+ :cfp => cfp,
424
+ :iseq => iseq,
425
+ }
426
+ file_path = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.absolute_path"))
427
+ if file_path.empty?
428
+ file_path = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.path"))
429
+ end
430
+ label = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.label"))
431
+ base_label = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.base_label"))
432
+ line_no = @ri.rb_vm_get_sourceline(cfp, iseq)
433
+
434
+ self_value = @gdb.cmd_get_value("p #{cfp}->self")
435
+ self_type = @ri.rb_type(self_value)
436
+ self_name = self_type == 'RUBY_T_CLASS' ? @gdb.cmd_get_value("p rb_class2name(#{cfp}->self)") : ''
437
+
438
+ func_prefix = "#{self_name}#" unless self_name.empty?
439
+
440
+ {
441
+ :callee => label.empty? ? '(unknown)' : "#{func_prefix}#{label}",
442
+ :args => '', # TODO: implement
443
+ :source_path_line => "#{file_path}:#{line_no}",
444
+ }
445
+ elsif @ri.rubyvm_cfunc_frame_p(cfp)
446
+ # C function
447
+
448
+ mid = @gdb.cmd_get_value("p #{cfp}->me->def ? #{cfp}->me->def->original_id : #{cfp}->me->called_id")
449
+ label = @ri.rb_id2str(mid)
450
+ if @prev_location
451
+ cfp = @prev_location[:cfp]
452
+ iseq = @prev_location[:iseq]
453
+
454
+ file_path = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.absolute_path"))
455
+ if file_path.empty?
456
+ file_path = @ri.rstring_ptr(@gdb.cmd_get_value("p #{iseq}->location.path"))
457
+ end
458
+ line_no = @ri.rb_vm_get_sourceline(cfp, iseq)
459
+ end
460
+
461
+ {
462
+ :callee => label,
463
+ :args => '', # TODO: implement
464
+ :source_path_line => "#{file_path}:#{line_no}",
465
+ }
466
+ end
467
+ end
468
+
469
+ def check_ruby_version
470
+ @ruby_version = @gdb.cmd_get_value('p ruby_version')
471
+ case @ruby_version.intern
472
+ when :'2.0.0'
473
+ end
474
+ raise "unknown ruby version" unless @ruby_version
475
+ end
476
+
477
+ def show_ruby_version
478
+ check_ruby_version
479
+ puts 'ruby_version:'
480
+ puts @ruby_version
481
+ puts ''
482
+ end
483
+
484
+ def show_backtrace
485
+ # TODO: List threads with ruby_current_vm->living_threads and dump all threads.
486
+ # Now, we dump only ruby_current_thread which is equivalent to ruby_current_vm->running_thread.
487
+
488
+ thread_map = get_map_of_ruby_thread_pointer_to_gdb_thread_id
489
+
490
+ # Detect ruby running thread and change gdb thread to it
491
+ current_thread_pointer = @gdb.cmd_get_pointer('p ruby_current_thread', 'rb_thread_t')
492
+ gdb_thread_id = thread_map[current_thread_pointer]
493
+ raise 'Cannot find current thread id in gdb' if gdb_thread_id.nil?
494
+ @gdb.cmd_exec("thread #{gdb_thread_id}")
495
+
496
+ # Show C backtrace
497
+ if @config['c_trace', true]
498
+ response = @gdb.cmd_exec('bt')
499
+ puts 'c_backtrace:'
500
+ response.each_line do |line|
501
+ break if line == '(gdb) '
502
+ puts line
503
+ end
504
+ puts ''
505
+ end
506
+
507
+ # Show Ruby backtrace
508
+ puts 'ruby_backtrace:'
509
+ cfp_count = @gdb.cmd_get_value('p (rb_control_frame_t *)(ruby_current_thread->stack + ruby_current_thread->stack_size) - ruby_current_thread->cfp').to_i
510
+
511
+ frame_infos = []
512
+ @prev_location = nil
513
+ # NOTE: @prev_location may not be set properly when limited by MAX_FRAMES
514
+ ([MAX_FRAMES, cfp_count].min - 1).downto(0).each do |count|
515
+ frame_info = get_ruby_frame(count)
516
+ frame_infos << frame_info if frame_info
517
+ end
518
+ frame_infos.reverse.each_with_index do |fi, i|
519
+ puts "[#{frame_infos.length - i}] #{fi[:callee]}(#{fi[:args]}) <- #{fi[:source_path_line]}"
520
+ end
521
+ end
522
+
523
+ end
524
+
525
+ def main
526
+ config = GDBRubyConfig.new(ARGV)
527
+ gdbruby = GDBRuby.new(config)
528
+ gdbruby.trace
529
+ end
530
+
531
+ main
532
+
533
+ # vim: set expandtab ts=2 sw=2 nowrap ft=ruby ff=unix :
@@ -0,0 +1,21 @@
1
+ #! /usr/bin/env gem build
2
+ # encoding: utf-8
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'gdbruby'
6
+ gem.version = File.read(File.expand_path('VERSION', File.dirname(__FILE__))).chomp
7
+ gem.authors = ["Tasuku SUENAGA"]
8
+ gem.email = ["tasuku-s-github@titech.ac"]
9
+ gem.homepage = "https://github.com/gunyarakun/gdbruby"
10
+ gem.summary = "gdbperl for Ruby"
11
+ gem.description = gem.summary
12
+ gem.licenses = ["New BSD"]
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "rspec"
21
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+ EXECUTABLE = File.join(File.dirname(__FILE__), '..', 'bin', 'gdbruby.rb')
5
+ TARGET = File.expand_path(File.join(File.dirname(__FILE__), 'target', 'call.rb'))
6
+
7
+ describe 'gdbruby' do
8
+ describe 'execute' do
9
+ RESULT = <<EOF
10
+ [13] sleep() <- #{TARGET}:42
11
+ [12] Module1::Class1#block in method6() <- #{TARGET}:42
12
+ [11] Module1::Class1#method7() <- #{TARGET}:47
13
+ [10] Module1::Class1#method6() <- #{TARGET}:40
14
+ [9] Module1::Class1#method5() <- #{TARGET}:35
15
+ [8] Module1::Class1#rescue in method4() <- #{TARGET}:26
16
+ [7] Module1::Class1#method4() <- #{TARGET}:23
17
+ [6] Module1::Class1#method3() <- (eval):1
18
+ [5] eval() <- #{TARGET}:17
19
+ [4] Module1::Class1#method3() <- #{TARGET}:17
20
+ [3] method2() <- #{TARGET}:12
21
+ [2] method1() <- #{TARGET}:7
22
+ [1] <main>() <- #{TARGET}:51
23
+ EOF
24
+
25
+ before(:all) do
26
+ @target_pid = Kernel.spawn(TARGET, :pgroup => true, :out => '/dev/null', :err => '/dev/null')
27
+ end
28
+
29
+ context 'With live process' do
30
+ before(:all) do
31
+ @output = `#{EXECUTABLE} #{@target_pid}`
32
+ end
33
+
34
+ it 'should work' do
35
+ @output =~ /ruby_backtrace:\n(.+)\z/m
36
+ ruby_backtrace = $1
37
+ expect(ruby_backtrace).to eq(RESULT)
38
+ end
39
+ end
40
+
41
+ gcore_path = `which gcore`.chomp!
42
+ if File.executable?(gcore_path)
43
+ context 'With core file' do
44
+ before(:all) do
45
+ @core_path = Tempfile.new('core')
46
+ puts "target_pid: #{@target_pid}"
47
+ system('gcore', '-o', @core_path.path, @target_pid.to_s)
48
+ @output = `#{EXECUTABLE} #{@target_pid}`
49
+ end
50
+
51
+ it 'should work' do
52
+ @output =~ /ruby_backtrace:\n(.+)\z/m
53
+ ruby_backtrace = $1
54
+ expect(ruby_backtrace).to eq(RESULT)
55
+ end
56
+
57
+ after(:all) do
58
+ @core_path.close!
59
+ end
60
+ end
61
+ end
62
+
63
+ after(:all) do
64
+ Process.kill('KILL', @target_pid)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+
3
+ RSpec.configure do |config|
4
+ config.expect_with :rspec do |c|
5
+ c.syntax = :expect
6
+ end
7
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set expandtab ts=2 sw=2 nowrap ft=ruby ff=unix : */
3
+
4
+ puts Process.pid
5
+
6
+ def method1(*args)
7
+ Module1::method2(*args)
8
+ end
9
+
10
+ module Module1
11
+ def self.method2(*args)
12
+ Class1::method3(*args)
13
+ end
14
+
15
+ class Class1
16
+ def self.method3(*args)
17
+ eval('method4(*args)')
18
+ end
19
+ end
20
+ end
21
+
22
+ def method4(*args)
23
+ begin
24
+ raise
25
+ rescue
26
+ method5(*args)
27
+ end
28
+ end
29
+
30
+ def method5(*args)
31
+ begin
32
+ raise
33
+ rescue
34
+ ensure
35
+ method6(*args)
36
+ end
37
+ end
38
+
39
+ def method6(*args)
40
+ method7(*args) do |*args|
41
+ p args
42
+ sleep(100)
43
+ end
44
+ end
45
+
46
+ def method7(*args)
47
+ yield *args
48
+ end
49
+
50
+ # TODO: test Struct, Bignum, File, Data, Match, Complex, Rational
51
+ method1(
52
+ nil,
53
+ true,
54
+ false,
55
+ :symbol,
56
+ 1,
57
+ Module1::Class1.new,
58
+ Module1::Class1,
59
+ Module1,
60
+ 1.0,
61
+ 'string',
62
+ /regex/,
63
+ ['array'],
64
+ { :hash => :value },
65
+ )
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gdbruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tasuku SUENAGA
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: gdbperl for Ruby
42
+ email:
43
+ - tasuku-s-github@titech.ac
44
+ executables:
45
+ - gdbruby.rb
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - .gitignore
50
+ - .travis.yml
51
+ - Gemfile
52
+ - README.md
53
+ - Rakefile
54
+ - VERSION
55
+ - bin/gdbruby.rb
56
+ - gdbruby.gemspec
57
+ - spec/gdbruby_spec.rb
58
+ - spec/spec_helper.rb
59
+ - spec/target/call.rb
60
+ homepage: https://github.com/gunyarakun/gdbruby
61
+ licenses:
62
+ - New BSD
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.0.3
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: gdbperl for Ruby
84
+ test_files:
85
+ - spec/gdbruby_spec.rb
86
+ - spec/spec_helper.rb
87
+ - spec/target/call.rb