rgdb 0.0.1.245

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. data/COPYING +676 -0
  2. data/ChangeLog +5 -0
  3. data/README +43 -0
  4. data/Rakefile +24 -0
  5. data/bin/rgdb +46 -0
  6. data/lib/gdb.rb +413 -0
  7. data/lib/rgdb.rb +75 -0
  8. data/lib/rgdb/version.rb +27 -0
  9. metadata +70 -0
@@ -0,0 +1,5 @@
1
+ = Revision history for rgdb
2
+
3
+ == 0.0.1 [2008-06-19]
4
+
5
+ * Birthday :-)
data/README ADDED
@@ -0,0 +1,43 @@
1
+ = rgdb - GDB inside IRB
2
+
3
+ == VERSION
4
+
5
+ This documentation refers to rgdb version 0.0.1
6
+
7
+
8
+ == DESCRIPTION
9
+
10
+ Runs Jamis Buck's GDB wrapper inside an IRB session. Inspired by Jonathan
11
+ Leighton's rgdb. But contrary to his solution, this one runs the IRB session
12
+ in the context of the GDB::Ruby object.
13
+
14
+ See
15
+ * http://weblog.jamisbuck.org/2006/9/25/gdb-wrapper-for-ruby
16
+ * http://dev.turnipspatch.com:9013/trac/browser/rgdb/trunk?rev=31
17
+
18
+
19
+ == AUTHORS
20
+
21
+ * Jens Wille <mailto:jens.wille@uni-koeln.de>
22
+
23
+
24
+ == LICENSE AND COPYRIGHT
25
+
26
+ Copyright (C) 2008 University of Cologne,
27
+ Albertus-Magnus-Platz, 50932 Cologne, Germany
28
+
29
+ rgdb is free software: you can redistribute it and/or modify it under the terms
30
+ of the GNU General Public License as published by the Free Software Foundation,
31
+ either version 3 of the License, or (at your option) any later version.
32
+
33
+ rgdb is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
34
+ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
35
+ PURPOSE. See the GNU General Public License for more details.
36
+
37
+ You should have received a copy of the GNU General Public License along with
38
+ rgdb. If not, see <http://www.gnu.org/licenses/>.
39
+
40
+ === GDB
41
+
42
+ The GDB library is copyright (C) 2006 Jamis Buck <mailto:jamis@37signals.com>,
43
+ released in the public domain.
@@ -0,0 +1,24 @@
1
+ require %q{lib/rgdb/version}
2
+
3
+ begin
4
+ require 'hen'
5
+
6
+ Hen.lay! {{
7
+ :rubyforge => {
8
+ :project => %q{prometheus},
9
+ :package => %q{rgdb}
10
+ },
11
+
12
+ :gem => {
13
+ :version => RGDB::VERSION,
14
+ :summary => %q{GDB inside IRB},
15
+ :files => FileList['lib/**/*.rb', 'bin/*'].to_a,
16
+ :extra_files => FileList['[A-Z]*'].to_a,
17
+ :dependencies => %w[]
18
+ }
19
+ }}
20
+ rescue LoadError
21
+ abort "Please install the 'hen' gem first."
22
+ end
23
+
24
+ ### Place your custom Rake tasks here.
@@ -0,0 +1,46 @@
1
+ #! /usr/bin/ruby
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # rgdb -- GDB inside IRB #
7
+ # #
8
+ # Copyright (C) 2008 University of Cologne, #
9
+ # Albertus-Magnus-Platz, #
10
+ # 50932 Cologne, Germany #
11
+ # #
12
+ # Authors: #
13
+ # Jens Wille <jens.wille@uni-koeln.de> #
14
+ # #
15
+ # rgdb is free software; you can redistribute it and/or modify it under the #
16
+ # terms of the GNU General Public License as published by the Free Software #
17
+ # Foundation; either version 3 of the License, or (at your option) any later #
18
+ # version. #
19
+ # #
20
+ # rgdb is distributed in the hope that it will be useful, but WITHOUT ANY #
21
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
22
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
23
+ # details. #
24
+ # #
25
+ # You should have received a copy of the GNU General Public License along #
26
+ # with rgdb. If not, see <http://www.gnu.org/licenses/>. #
27
+ # #
28
+ ###############################################################################
29
+ #++
30
+
31
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
32
+
33
+ require 'rgdb'
34
+
35
+ USAGE = "Usage: #{$0} [-h|--help] <pid>"
36
+
37
+ case arg = ARGV.shift
38
+ when '-h', '--help'
39
+ puts USAGE
40
+ when '--version'
41
+ puts "rgdb v#{RGDB::VERSION}"
42
+ when /\A\d+\z/
43
+ RGDB.start(arg, __FILE__)
44
+ else
45
+ abort USAGE
46
+ end
@@ -0,0 +1,413 @@
1
+ # gdb.rb -- a simple wrapper around the GDB console to make inspecting
2
+ # ruby processes less painful.
3
+ #
4
+ # Author: Jamis Buck <jamis@37signals.com>
5
+ #
6
+ # This library is in the public domain. You are free to use it, modify
7
+ # it, redistribute it, or ignore it, as you wish, with absolutely no
8
+ # restrictions.
9
+ #
10
+ # CAVEATS:
11
+ # * The current incarnation is not very error tolerant. If you run
12
+ # into problems, try running it with the $gdb_version variable set
13
+ # to true.
14
+ # * Sometimes, the current implementation will kill the process it
15
+ # attaches to. I suspect it has something to do with garbage
16
+ # collection kicking in... I leave it as an exercise for the reader.
17
+ #
18
+ # See the bottom of this file for a handful of unit tests.
19
+
20
+ require 'rbconfig'
21
+
22
+ class Integer
23
+ # Monkey patch the Integer class to make it easier to compute
24
+ # word offsets. "5.ints" gives you the size (in bytes) of 5
25
+ # integers (words) on the current platform.
26
+ def ints
27
+ self * 1.size
28
+ end
29
+ end
30
+
31
+ # The GDB module provides a "raw", low-level, language-independent
32
+ # interface to GDB (GDB::Interface) as well as a higher-level
33
+ # interface specifically for Ruby programs (GDB::Ruby). Theoretically,
34
+ # GDB::Interface could be extended to support other languages
35
+ # than Ruby.
36
+ module GDB
37
+ # This module is used to extend objects (like Strings) that originate
38
+ # in the remote instance. This lets us reuse them easily in subsequent
39
+ # calls to the remote instance.
40
+ module RemoteObject
41
+ attr_accessor :address
42
+ end
43
+
44
+ # The basic interface for wrapping GDB. You could concievably use this
45
+ # to interace with arbitrary remote processes, though it is best used
46
+ # by extending it to add platform-specific functionality.
47
+ class Interface
48
+ # Creates a new GDB::Interface that connects to process-id and
49
+ # executable.
50
+ def initialize(executable, pid)
51
+ @gdb = IO.popen("gdb -q #{executable} #{pid} 2>&1", "r+")
52
+ read_to_prompt
53
+ end
54
+
55
+ # Returns the (32-bit) integer at the given address
56
+ def int_at(address)
57
+ command("x/1dw #{address}").first.split(/:/).last.to_i
58
+ end
59
+
60
+ # Returns the string located at the given address
61
+ def string_at(address)
62
+ command("x/1s *#{address}").first.match(/"(.*)"/)[1]
63
+ end
64
+
65
+ # Returns the (8-bit) double precision floating point value at
66
+ # the given address.
67
+ def double_at(address)
68
+ command("x/1fg #{address}").first.split(/:/).last.to_f
69
+ end
70
+
71
+ # Executes the given command and returns the output as an array
72
+ # of lines. The command must be in GDB's syntax. If $gdb_verbose
73
+ # is true, this will print the command to STDOUT as well.
74
+ def command(cmd)
75
+ puts "(gdb) #{cmd}" if $gdb_verbose
76
+ @gdb.puts(cmd.strip)
77
+ read_to_prompt
78
+ end
79
+
80
+ # Call a C function. The +function+ parameter must include any
81
+ # parameters, all of which must be in valid C syntax. +opts+
82
+ # should include a <code>:return</code> key, which indicates
83
+ # what the expected return type of the function is:
84
+ #
85
+ # * <code>:int</code>
86
+ # * <code>:string</code>
87
+ # * <code>:void</code>
88
+ #
89
+ # Currently, the default is <code>:void</code>. The method
90
+ # returns a value of the expected type.
91
+ def call(function, opts={})
92
+ cast = case opts[:return]
93
+ when :int then "int"
94
+ when :string then "char*"
95
+ when nil, :void then "void"
96
+ else Kernel.raise "unsupported return type #{opts[:return].inspect}"
97
+ end
98
+
99
+ result = command("call (#{cast})#{function}")
100
+
101
+ case opts[:return]
102
+ when :int
103
+ if md = result.first.match(/= (-?\d+)/)
104
+ md[1].to_i
105
+ else
106
+ Kernel.raise "couldn't match integer result from #{result.inspect}"
107
+ end
108
+ when :string then result.first.match(/"(.*)"/)[1]
109
+ else nil
110
+ end
111
+ end
112
+
113
+ # Reads all lines emitted by GDB until a recognized prompt is encountered.
114
+ # Currently recognized prompts are:
115
+ #
116
+ # * "(gdb) "
117
+ # * " >"
118
+ #
119
+ # If your version of GDB uses a different kind of prompt, you should
120
+ # modify this method to include it.
121
+ #
122
+ # This method returns the lines read as an array. If $gdb_verbose is
123
+ # true, the lines will be echoed to STDOUT.
124
+ def read_to_prompt
125
+ lines = []
126
+ line = ""
127
+ while result = IO.select([@gdb])
128
+ next if result.empty?
129
+ c = @gdb.read(1)
130
+ break if c.nil?
131
+ line << c
132
+ break if line == "(gdb) " || line == " >"
133
+ if line[-1] == ?\n
134
+ lines << line
135
+ line = ""
136
+ end
137
+ end
138
+ puts lines.map { |l| "> #{l}" } if $gdb_verbose
139
+ lines
140
+ end
141
+
142
+ # A convenience method for executing GDB commands. Any unrecognized
143
+ # message is packaged up and invoked by GDB.
144
+ def method_missing(sym, *args)
145
+ cmd = sym.to_s
146
+ cmd << " #{args.join(' ')}" unless args.empty?
147
+ command(cmd)
148
+ end
149
+ end
150
+
151
+ # This is an extension of the Interface class that implements Ruby-specific
152
+ # functionality.
153
+ class Ruby < Interface
154
+ # A wrapper for a remote Object instance.
155
+ class Object
156
+ attr_reader :gdb
157
+ attr_reader :address
158
+
159
+ # Given a gdb instance and an address, determine what type of Object
160
+ # is at the given address, and return it.
161
+ def self.from_ptr(gdb, address)
162
+ case
163
+ when (address & 0x01) == 1 then address >> 1
164
+ when address == 0 then false
165
+ when address == 2 then true
166
+ when address == 4 then nil
167
+ when (address & 0xff) == 0x0e then gdb.id2name(address)
168
+ else
169
+ case (type = (gdb.int_at(address) & 0x3f))
170
+ when 2 then Object.new(gdb, address)
171
+ when 6 then gdb.double_at(address+2.ints)
172
+ when 7 then
173
+ string = gdb.string_at(address+3.ints)
174
+ string.extend(RemoteObject)
175
+ string.address = address
176
+ string
177
+ when 9 then Array.new(gdb, address)
178
+ when 11 then Hash.new(gdb, address)
179
+ else Object.new(gdb, address)
180
+ end
181
+ end
182
+ end
183
+
184
+ # Given a value, return an integer representing the VALUE of the
185
+ # object on the remote instance.
186
+ def to_gdb_value(value)
187
+ return value.address if value.respond_to?(:address)
188
+
189
+ case value
190
+ when Fixnum then ((value << 1)+1).to_s
191
+ when false then "0"
192
+ when true then "2"
193
+ when nil then "4"
194
+ when String then gdb.call("rb_str_new2(#{value.inspect})", :return => :int).to_s
195
+ else Kernel.raise "not supported yet: #{value.class.name}"
196
+ end
197
+ end
198
+
199
+ # Creates a new remote Object reference for the given gdb instance
200
+ # and address.
201
+ def initialize(gdb, address)
202
+ @gdb = gdb
203
+ @address = address.to_i
204
+ end
205
+
206
+ # Invokes a method on the remote Object reference, returning the
207
+ # result as another Object.
208
+ def send(sym, *args)
209
+ command = "rb_funcall(#{address}, #{gdb.intern(sym.to_s)}, #{args.length}"
210
+ args = args.map { |arg| to_gdb_value(arg) }.join(", ")
211
+ command << ", #{args}" unless args.empty?
212
+ command << ")"
213
+
214
+ gdb.object_from(command)
215
+ end
216
+
217
+ # A convenience method for #send
218
+ def method_missing(sym, *args)
219
+ send(sym, *args)
220
+ end
221
+ end
222
+
223
+ # A representation of a remote Array instance.
224
+ class Array < Object
225
+ # Query the length of the remote Array instance
226
+ def length
227
+ @length ||= gdb.int_at(address+2.ints)
228
+ end
229
+
230
+ # Return the object at the given index
231
+ def [](index)
232
+ gdb.object_from("rb_ary_entry(#{address}, #{index})")
233
+ end
234
+
235
+ # Iterate over each element of the remote array
236
+ def each
237
+ length.times { |idx| yield self[idx] }
238
+ end
239
+
240
+ # Convert the remote Array into a local (true) Array instance.
241
+ def to_ary
242
+ ary = []
243
+ each { |item| ary << item }
244
+ ary
245
+ end
246
+ end
247
+
248
+ # Wraps a remote Hash instance. All it really provides, though, is a
249
+ # way to build a local Hash instance that mirrors it.
250
+ class Hash < Object
251
+ # Return a ::Hash instance that has the same keys and values as
252
+ # the remote hash instance. Default values are not preserved.
253
+ def to_h
254
+ keys.to_ary.inject({}) do |hash, key|
255
+ hash[key] = self[key]
256
+ hash
257
+ end
258
+ end
259
+ end
260
+
261
+ # Create a new GDB::Ruby instance that attaches to the given
262
+ # process ID. By default, the same ruby interpreter is used
263
+ # as is running the GDB::Ruby script, but you can override
264
+ # that by using the second parameter.
265
+ def initialize(pid, ruby=nil)
266
+ ruby ||= File.join(Config::CONFIG["bindir"],
267
+ Config::CONFIG["RUBY_INSTALL_NAME"] +
268
+ Config::CONFIG["EXEEXT"])
269
+ @interns = {}
270
+ @names = {}
271
+ super(ruby, pid)
272
+ end
273
+
274
+ # Returns the symbol for the given numberic ID, using the remote
275
+ # environment.
276
+ def id2name(id)
277
+ @names[id] ||= call("rb_id2name(#{id >> 8})", :return => :string).to_sym
278
+ end
279
+
280
+ # Returns the numeric ID on the remote host that represents the
281
+ # internalized string.
282
+ def intern(string)
283
+ @interns[string] ||= call("rb_intern(#{string.inspect})", :return => :int)
284
+ end
285
+
286
+ # Executes the given command (C API) and returns the result as a wrapped
287
+ # GDB::Ruby::Object.
288
+ def object_from(cmd)
289
+ Object.from_ptr(self, call("#{cmd}", :return => :int))
290
+ end
291
+
292
+ # Evaluates the given string in the remote environment, returning the
293
+ # resulting object. Exceptions are silently ignored.
294
+ def eval(string)
295
+ object_from("rb_eval_string_protect(#{string.inspect}, (int*)0)")
296
+ end
297
+
298
+ # Returns a hash of the local variables that are active in the remote
299
+ # process, at it's current scope.
300
+ def local_variables
301
+ eval("local_variables").to_ary.inject({}) do |hash, name|
302
+ hash[name] = eval(name)
303
+ hash
304
+ end
305
+ end
306
+
307
+ # Returns an array of the backtrace of the remote process.
308
+ def backtrace
309
+ object_from("backtrace(-1)").to_ary
310
+ end
311
+
312
+ # Raises an exception in the remote process. The +exc+ parameter
313
+ # must be one of the core exception classes, like Exception,
314
+ # RuntimeError, and so forth.
315
+ def raise(exc, message)
316
+ call "rb_raise((int)rb_e#{exc}, #{message.inspect})"
317
+ end
318
+
319
+ # Returns a hash of the classes known to the remote instance,
320
+ # with the number of instances of each of the classes.
321
+ def object_space
322
+ eval("h={}; ObjectSpace.each_object { |obj| h[obj.class.name] ||= 0; h[obj.class.name] += 1 }; h").to_h
323
+ end
324
+ end
325
+ end
326
+
327
+ if __FILE__ == $0
328
+ require 'test/unit'
329
+
330
+ SENTINEL = "/tmp/gdb-ruby-test.pid"
331
+ FLAG_FILE = "/tmp/gdb-ruby-test.flag"
332
+
333
+ SCRIPT = <<EOS
334
+ class ZxyVlqrt; end
335
+ glooble = ZxyVlqrt.new
336
+
337
+ def a(n)
338
+ b(n, n+1)
339
+ end
340
+
341
+ def b(x, y)
342
+ z = x + y
343
+ File.open("#{SENTINEL}", "w") { |f| f.write($$) }
344
+ loop { sleep 1 }
345
+ end
346
+
347
+ begin
348
+ a(5)
349
+ rescue RuntimeError => e
350
+ File.open("#{FLAG_FILE}", "w") { |f| f.write(e.message) }
351
+ end
352
+ EOS
353
+
354
+ SCRIPT_NAME = "/tmp/gdb-ruby-test.rb"
355
+
356
+ class GDBRubyTest < Test::Unit::TestCase
357
+ def setup
358
+ File.open(SCRIPT_NAME, "w") { |f| f.write(SCRIPT) }
359
+ start_script
360
+ @gdb = GDB::Ruby.new(@pid)
361
+ end
362
+
363
+ def teardown
364
+ @gdb.quit
365
+ kill_script
366
+ File.delete(FLAG_FILE) rescue nil
367
+ File.delete(SENTINEL) rescue nil
368
+ File.delete(SCRIPT_NAME) rescue nil
369
+ end
370
+
371
+ def test_local_variables
372
+ expect = {"x" => 5, "y" => 6, "z" => 11}
373
+ assert_equal(expect, @gdb.local_variables)
374
+ end
375
+
376
+ def test_backtrace
377
+ expect = ["#{SCRIPT_NAME}:11:in `sleep'",
378
+ "#{SCRIPT_NAME}:11:in `b'",
379
+ "#{SCRIPT_NAME}:11:in `b'",
380
+ "#{SCRIPT_NAME}:5:in `a'",
381
+ "#{SCRIPT_NAME}:15"]
382
+
383
+ assert_equal expect, @gdb.backtrace
384
+ end
385
+
386
+ def test_raise
387
+ assert !File.exist?(FLAG_FILE)
388
+ @gdb.raise RuntimeError, "going boom"
389
+ assert File.exist?(FLAG_FILE)
390
+ assert_equal "going boom", File.read(FLAG_FILE)
391
+ end
392
+
393
+ def test_object_space
394
+ space = @gdb.object_space
395
+ assert_equal 1, space["ZxyVlqrt"]
396
+ end
397
+
398
+ private
399
+
400
+ def start_script
401
+ if (@pid = fork)
402
+ sleep 0.1 while !File.exist?(SENTINEL)
403
+ return
404
+ end
405
+
406
+ exec "ruby", SCRIPT_NAME
407
+ end
408
+
409
+ def kill_script
410
+ Process.kill("TERM", @pid)
411
+ end
412
+ end
413
+ end