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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +10 -0
- data/Gemfile +3 -0
- data/README.md +68 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/bin/gdbruby.rb +533 -0
- data/gdbruby.gemspec +21 -0
- data/spec/gdbruby_spec.rb +67 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/target/call.rb +65 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/gdbruby.rb
ADDED
@@ -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 :
|
data/gdbruby.gemspec
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
data/spec/target/call.rb
ADDED
@@ -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
|