rgdb 0.0.1.245

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.
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