ruby-gdb 0.1.0
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/bake/ruby/gdb.rb +135 -0
- data/context/fiber-debugging.md +372 -0
- data/context/getting-started.md +167 -0
- data/context/heap-debugging.md +426 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +272 -0
- data/context/stack-inspection.md +357 -0
- data/data/ruby/gdb/command.py +254 -0
- data/data/ruby/gdb/constants.py +59 -0
- data/data/ruby/gdb/fiber.py +825 -0
- data/data/ruby/gdb/format.py +201 -0
- data/data/ruby/gdb/heap.py +563 -0
- data/data/ruby/gdb/init.py +25 -0
- data/data/ruby/gdb/object.py +85 -0
- data/data/ruby/gdb/rarray.py +124 -0
- data/data/ruby/gdb/rbasic.py +103 -0
- data/data/ruby/gdb/rbignum.py +52 -0
- data/data/ruby/gdb/rclass.py +133 -0
- data/data/ruby/gdb/rexception.py +150 -0
- data/data/ruby/gdb/rfloat.py +95 -0
- data/data/ruby/gdb/rhash.py +157 -0
- data/data/ruby/gdb/rstring.py +217 -0
- data/data/ruby/gdb/rstruct.py +157 -0
- data/data/ruby/gdb/rsymbol.py +291 -0
- data/data/ruby/gdb/stack.py +609 -0
- data/data/ruby/gdb/value.py +181 -0
- data/lib/ruby/gdb/version.rb +13 -0
- data/lib/ruby/gdb.rb +23 -0
- data/license.md +21 -0
- data/readme.md +42 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 631791caaf97095f10517f1dd754921143a8f6384cfabea6cd68e24ce71f15fa
|
|
4
|
+
data.tar.gz: 6049e59f42b890adc927bacfe16b7319e288f93d0c7e2b1454d9971fb2f0d299
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e550b09ea2c326b506c18bc96a619c1af2dccef8e19cb068addb5cff0985409c271f83b7ba34eb68532e845dda1c8abf30c2c4ec29fa3f65b9fc3c29b49e147b
|
|
7
|
+
data.tar.gz: 55517867200278ccb455091b08339594b4b5611ce20fe84ecc66df68da61e4acc05999c6dbc221253d31ae5144afdbf14ad35f512cdb347725f929e0ee448d4a
|
data/bake/ruby/gdb.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "ruby/gdb"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Install GDB extensions by adding source line to ~/.gdbinit
|
|
10
|
+
# @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
|
|
11
|
+
def install(gdbinit: nil)
|
|
12
|
+
gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
|
|
13
|
+
init_py_path = Ruby::GDB.init_script_path
|
|
14
|
+
source_line = "source #{init_py_path}"
|
|
15
|
+
marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
|
|
16
|
+
|
|
17
|
+
puts "Installing Ruby GDB extensions..."
|
|
18
|
+
puts " Extensions: #{File.dirname(init_py_path)}"
|
|
19
|
+
puts " Config: #{gdbinit_path}"
|
|
20
|
+
|
|
21
|
+
# Read existing .gdbinit or create empty array
|
|
22
|
+
lines = File.exist?(gdbinit_path) ? File.readlines(gdbinit_path) : []
|
|
23
|
+
|
|
24
|
+
# Check if already installed (look for marker comment)
|
|
25
|
+
marker_index = lines.index{|line| line.strip == marker_comment}
|
|
26
|
+
|
|
27
|
+
if marker_index
|
|
28
|
+
# Already installed - update the source line in case path changed
|
|
29
|
+
source_index = marker_index + 1
|
|
30
|
+
if source_index < lines.size && lines[source_index].strip.start_with?("source")
|
|
31
|
+
old_source = lines[source_index].strip
|
|
32
|
+
if old_source == source_line
|
|
33
|
+
puts "\n✓ Already installed in #{gdbinit_path}"
|
|
34
|
+
puts " #{source_line}"
|
|
35
|
+
return
|
|
36
|
+
else
|
|
37
|
+
# Path changed - update it
|
|
38
|
+
lines[source_index] = "#{source_line}\n"
|
|
39
|
+
File.write(gdbinit_path, lines.join)
|
|
40
|
+
puts "\n✓ Updated installation in #{gdbinit_path}"
|
|
41
|
+
puts " Old: #{old_source}"
|
|
42
|
+
puts " New: #{source_line}"
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Not installed - add it
|
|
49
|
+
File.open(gdbinit_path, "a") do |f|
|
|
50
|
+
f.puts unless lines.last&.strip&.empty?
|
|
51
|
+
f.puts marker_comment
|
|
52
|
+
f.puts source_line
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
puts "\n✓ Installation complete!"
|
|
56
|
+
puts "\nAdded to #{gdbinit_path}:"
|
|
57
|
+
puts " #{source_line}"
|
|
58
|
+
puts "\nExtensions will load automatically when you start GDB."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Uninstall GDB extensions by removing source line from ~/.gdbinit
|
|
62
|
+
# @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
|
|
63
|
+
def uninstall(gdbinit: nil)
|
|
64
|
+
gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
|
|
65
|
+
marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
|
|
66
|
+
|
|
67
|
+
puts "Uninstalling Ruby GDB extensions..."
|
|
68
|
+
|
|
69
|
+
unless File.exist?(gdbinit_path)
|
|
70
|
+
puts "No ~/.gdbinit file found - nothing to uninstall."
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
lines = File.readlines(gdbinit_path)
|
|
75
|
+
marker_index = lines.index{|line| line.strip == marker_comment}
|
|
76
|
+
|
|
77
|
+
unless marker_index
|
|
78
|
+
puts "Extensions were not found in #{gdbinit_path}"
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Remove the marker comment and the source line after it
|
|
83
|
+
lines.delete_at(marker_index) # Remove comment
|
|
84
|
+
if marker_index < lines.size && lines[marker_index].strip.start_with?("source")
|
|
85
|
+
removed_line = lines.delete_at(marker_index).strip # Remove source line
|
|
86
|
+
puts " Removed: #{removed_line}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Clean up empty line before marker if it exists
|
|
90
|
+
if marker_index > 0 && lines[marker_index - 1].strip.empty?
|
|
91
|
+
lines.delete_at(marker_index - 1)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
File.write(gdbinit_path, lines.join)
|
|
95
|
+
puts "✓ Removed Ruby GDB extensions from #{gdbinit_path}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Show installation information
|
|
99
|
+
# @parameter gdbinit [String] Optional path to .gdbinit (defaults to ~/.gdbinit)
|
|
100
|
+
def info(gdbinit: nil)
|
|
101
|
+
gdbinit_path = gdbinit || File.join(Dir.home, ".gdbinit")
|
|
102
|
+
init_py_path = Ruby::GDB.init_script_path
|
|
103
|
+
marker_comment = "# Ruby GDB Extensions (ruby-gdb gem)"
|
|
104
|
+
|
|
105
|
+
puts "Ruby GDB Extensions v#{Ruby::GDB::VERSION}"
|
|
106
|
+
puts "\nGem data directory: #{Ruby::GDB.data_path}"
|
|
107
|
+
puts "Init script: #{init_py_path}"
|
|
108
|
+
|
|
109
|
+
# Check installation status by looking for marker comment
|
|
110
|
+
installed = false
|
|
111
|
+
current_source = nil
|
|
112
|
+
|
|
113
|
+
if File.exist?(gdbinit_path)
|
|
114
|
+
lines = File.readlines(gdbinit_path)
|
|
115
|
+
marker_index = lines.index{|line| line.strip == marker_comment}
|
|
116
|
+
if marker_index
|
|
117
|
+
installed = true
|
|
118
|
+
source_index = marker_index + 1
|
|
119
|
+
if source_index < lines.size
|
|
120
|
+
current_source = lines[source_index].strip
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
puts "\nGDB config: #{gdbinit_path}"
|
|
126
|
+
if installed
|
|
127
|
+
puts "Status: ✓ Installed"
|
|
128
|
+
if current_source
|
|
129
|
+
puts " #{current_source}"
|
|
130
|
+
end
|
|
131
|
+
else
|
|
132
|
+
puts "Status: ✗ Not installed"
|
|
133
|
+
puts "\nRun: bake ruby:gdb:install"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# Fiber Debugging
|
|
2
|
+
|
|
3
|
+
This guide explains how to debug Ruby fibers using GDB, including inspecting fiber state, backtraces, and switching between fiber contexts.
|
|
4
|
+
|
|
5
|
+
## Why Fiber Debugging is Critical
|
|
6
|
+
|
|
7
|
+
When debugging concurrent Ruby applications, fibers can be in various states - running, suspended, or terminated. Unlike traditional debugging where you see one call stack, fiber-based programs have multiple execution contexts simultaneously. Understanding what each fiber is doing and why it stopped is essential for diagnosing deadlocks, exceptions, and state corruption.
|
|
8
|
+
|
|
9
|
+
Use fiber debugging when you need:
|
|
10
|
+
|
|
11
|
+
- **Diagnose deadlocks**: See which fibers are waiting and on what
|
|
12
|
+
- **Find hidden exceptions**: Discover exceptions in suspended fibers that haven't propagated yet
|
|
13
|
+
- **Understand concurrency**: Visualize what all fibers are doing at a point in time
|
|
14
|
+
- **Debug async code**: Navigate between fiber contexts to trace execution flow
|
|
15
|
+
|
|
16
|
+
## Scanning for Fibers
|
|
17
|
+
|
|
18
|
+
The first step in fiber debugging is finding all fibers in the heap.
|
|
19
|
+
|
|
20
|
+
### Basic Scan
|
|
21
|
+
|
|
22
|
+
Scan the entire Ruby heap for fiber objects:
|
|
23
|
+
|
|
24
|
+
~~~
|
|
25
|
+
(gdb) rb-scan-fibers
|
|
26
|
+
Scanning 1250 heap pages...
|
|
27
|
+
Checked 45000 objects, found 12 fiber(s)...
|
|
28
|
+
Scan complete: checked 67890 objects
|
|
29
|
+
|
|
30
|
+
Found 12 fiber(s):
|
|
31
|
+
|
|
32
|
+
Fiber #0: 0x7f8a1c800000
|
|
33
|
+
Status: RESUMED
|
|
34
|
+
Stack: base=0x7f8a1d000000, size=1048576
|
|
35
|
+
VM Stack: 0x7f8a1c900000, size=4096
|
|
36
|
+
CFP: 0x7f8a1c901000
|
|
37
|
+
|
|
38
|
+
Fiber #1: 0x7f8a1c800100
|
|
39
|
+
Status: SUSPENDED
|
|
40
|
+
Exception: RuntimeError: Connection failed
|
|
41
|
+
...
|
|
42
|
+
~~~
|
|
43
|
+
|
|
44
|
+
### Limiting Results
|
|
45
|
+
|
|
46
|
+
For large applications, limit the scan:
|
|
47
|
+
|
|
48
|
+
~~~
|
|
49
|
+
(gdb) rb-scan-fibers 10 # Find first 10 fibers only
|
|
50
|
+
~~~
|
|
51
|
+
|
|
52
|
+
### Caching Results
|
|
53
|
+
|
|
54
|
+
Cache fiber addresses for faster subsequent access:
|
|
55
|
+
|
|
56
|
+
~~~
|
|
57
|
+
(gdb) rb-scan-fibers --cache # Save to fibers.json
|
|
58
|
+
(gdb) rb-scan-fibers --cache my.json # Custom cache file
|
|
59
|
+
~~~
|
|
60
|
+
|
|
61
|
+
Later, load from cache instantly:
|
|
62
|
+
|
|
63
|
+
~~~
|
|
64
|
+
(gdb) rb-scan-fibers --cache
|
|
65
|
+
Loaded 12 fiber address(es) from fibers.json
|
|
66
|
+
~~~
|
|
67
|
+
|
|
68
|
+
This is especially useful with core dumps where heap scanning is slow.
|
|
69
|
+
|
|
70
|
+
## Inspecting Specific Fibers
|
|
71
|
+
|
|
72
|
+
After scanning, inspect fibers by index:
|
|
73
|
+
|
|
74
|
+
### Fiber Overview
|
|
75
|
+
|
|
76
|
+
~~~
|
|
77
|
+
(gdb) rb-fiber 5
|
|
78
|
+
Fiber #5: 0x7f8a1c800500
|
|
79
|
+
Status: SUSPENDED
|
|
80
|
+
Exception: IOError: Connection reset
|
|
81
|
+
Stack: base=0x7f8a1e000000, size=1048576
|
|
82
|
+
VM Stack: 0x7f8a1c950000, size=4096
|
|
83
|
+
CFP: 0x7f8a1c951000
|
|
84
|
+
EC: 0x7f8a1c800600
|
|
85
|
+
|
|
86
|
+
Available commands:
|
|
87
|
+
rb-fiber-bt 5 - Show backtrace
|
|
88
|
+
rb-fiber-vm-stack 5 - Show VM stack
|
|
89
|
+
rb-fiber-c-stack 5 - Show C/machine stack info
|
|
90
|
+
~~~
|
|
91
|
+
|
|
92
|
+
This shows at a glance whether the fiber has an exception and what its state is.
|
|
93
|
+
|
|
94
|
+
### Fiber Backtraces
|
|
95
|
+
|
|
96
|
+
See the Ruby-level call stack:
|
|
97
|
+
|
|
98
|
+
~~~
|
|
99
|
+
(gdb) rb-fiber-bt 5
|
|
100
|
+
Backtrace for fiber 0x7f8a1c800500:
|
|
101
|
+
45: /app/lib/connection.rb:123:in `read'
|
|
102
|
+
44: /app/lib/connection.rb:89:in `receive'
|
|
103
|
+
43: /app/lib/server.rb:56:in `handle_client'
|
|
104
|
+
42: /app/lib/server.rb:34:in `block in run'
|
|
105
|
+
41: /gems/async-2.0/lib/async/task.rb:45:in `run'
|
|
106
|
+
...
|
|
107
|
+
~~~
|
|
108
|
+
|
|
109
|
+
Show backtraces for all non-terminated fibers:
|
|
110
|
+
|
|
111
|
+
~~~
|
|
112
|
+
(gdb) rb-all-fiber-bt
|
|
113
|
+
~~~
|
|
114
|
+
|
|
115
|
+
## Switching Fiber Context
|
|
116
|
+
|
|
117
|
+
The most powerful feature: switch GDB's view to a fiber's stack (even in core dumps!).
|
|
118
|
+
|
|
119
|
+
### Basic Usage
|
|
120
|
+
|
|
121
|
+
~~~
|
|
122
|
+
(gdb) rb-scan-fibers
|
|
123
|
+
(gdb) rb-fiber-switch 5
|
|
124
|
+
Switched to Fiber #5: 0x7f8a1c800500
|
|
125
|
+
Status: SUSPENDED
|
|
126
|
+
Exception: IOError: Connection reset
|
|
127
|
+
|
|
128
|
+
Convenience variables set:
|
|
129
|
+
$fiber = Current fiber (struct rb_fiber_struct *)
|
|
130
|
+
$ec = Execution context (rb_execution_context_t *)
|
|
131
|
+
$errinfo = Exception being handled (VALUE)
|
|
132
|
+
|
|
133
|
+
Now try:
|
|
134
|
+
bt # Show C backtrace of fiber
|
|
135
|
+
frame <n> # Switch to frame N
|
|
136
|
+
info locals # Show local variables
|
|
137
|
+
rp $errinfo # Pretty print exception
|
|
138
|
+
~~~
|
|
139
|
+
|
|
140
|
+
After switching, all standard GDB commands work with the fiber's context:
|
|
141
|
+
|
|
142
|
+
~~~
|
|
143
|
+
(gdb) bt # C backtrace of fiber
|
|
144
|
+
#0 0x00007f8a1c567890 in fiber_setcontext
|
|
145
|
+
#1 0x00007f8a1c567900 in rb_fiber_yield
|
|
146
|
+
#2 0x00007f8a1c234567 in rb_io_wait_readable
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
(gdb) frame 2
|
|
150
|
+
(gdb) info locals # Local C variables in that frame
|
|
151
|
+
(gdb) rb-object-print $ec->cfp->sp[-1] # Ruby values on VM stack
|
|
152
|
+
~~~
|
|
153
|
+
|
|
154
|
+
### Switching Back
|
|
155
|
+
|
|
156
|
+
Return to normal stack view:
|
|
157
|
+
|
|
158
|
+
~~~
|
|
159
|
+
(gdb) rb-fiber-switch off
|
|
160
|
+
Fiber unwinder deactivated. Switched back to normal stack view.
|
|
161
|
+
~~~
|
|
162
|
+
|
|
163
|
+
## Analyzing Fiber State
|
|
164
|
+
|
|
165
|
+
### VM Stack Inspection
|
|
166
|
+
|
|
167
|
+
View the Ruby VM stack for a fiber:
|
|
168
|
+
|
|
169
|
+
~~~
|
|
170
|
+
(gdb) rb-fiber-vm-stack 5
|
|
171
|
+
VM Stack for Fiber #5:
|
|
172
|
+
Base: 0x7f8a1c950000
|
|
173
|
+
Size: 4096 VALUEs (32768 bytes)
|
|
174
|
+
CFP: 0x7f8a1c951000
|
|
175
|
+
~~~
|
|
176
|
+
|
|
177
|
+
### VM Control Frames
|
|
178
|
+
|
|
179
|
+
Walk through each Ruby method frame:
|
|
180
|
+
|
|
181
|
+
~~~
|
|
182
|
+
(gdb) rb-fiber-vm-frames 5
|
|
183
|
+
VM Control Frames for Fiber #5:
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
Frame #0 (depth 45):
|
|
187
|
+
CFP Address: 0x7f8a1c951000
|
|
188
|
+
PC: 0x7f8a1c234500
|
|
189
|
+
SP: 0x7f8a1c950100
|
|
190
|
+
Location: /app/lib/connection.rb:123
|
|
191
|
+
Method: read
|
|
192
|
+
Frame Type: VM_FRAME_MAGIC_METHOD
|
|
193
|
+
Stack Depth: 256 slots
|
|
194
|
+
~~~
|
|
195
|
+
|
|
196
|
+
### Stack Top Values
|
|
197
|
+
|
|
198
|
+
See what's on top of the VM stack:
|
|
199
|
+
|
|
200
|
+
~~~
|
|
201
|
+
(gdb) rb-fiber-stack-top 5 20
|
|
202
|
+
VM Stack Top for Fiber #5:
|
|
203
|
+
|
|
204
|
+
Top 20 VALUE(s) on stack (newest first):
|
|
205
|
+
|
|
206
|
+
[ -1] 0x00007f8a1c888888 T_STRING "Hello"
|
|
207
|
+
[ -2] 0x0000000000000015 Fixnum(10) Fixnum: 10
|
|
208
|
+
[ -3] 0x00007f8a1c999999 T_HASH <hash:0x7f8a1c999999>
|
|
209
|
+
...
|
|
210
|
+
~~~
|
|
211
|
+
|
|
212
|
+
## Diagnosing Crashes
|
|
213
|
+
|
|
214
|
+
When Ruby crashes, find out why:
|
|
215
|
+
|
|
216
|
+
~~~
|
|
217
|
+
(gdb) rb-diagnose-exit
|
|
218
|
+
================================================================================
|
|
219
|
+
DIAGNOSING RUBY EXIT/CRASH
|
|
220
|
+
================================================================================
|
|
221
|
+
|
|
222
|
+
[1] Main Thread Exception:
|
|
223
|
+
--------------------------------------------------------------------------------
|
|
224
|
+
Main thread has exception: NoMethodError
|
|
225
|
+
VALUE: 0x7f8a1c777777
|
|
226
|
+
Use: rb-object-print (VALUE)0x7f8a1c777777
|
|
227
|
+
|
|
228
|
+
[2] Fibers with Exceptions:
|
|
229
|
+
--------------------------------------------------------------------------------
|
|
230
|
+
Fiber #5 (SUSPENDED): RuntimeError
|
|
231
|
+
Fiber: 0x7f8a1c800500, errinfo: 0x7f8a1c666666
|
|
232
|
+
Fiber #8 (SUSPENDED): IOError
|
|
233
|
+
Fiber: 0x7f8a1c800800, errinfo: 0x7f8a1c555555
|
|
234
|
+
|
|
235
|
+
[3] Interrupt Flags:
|
|
236
|
+
--------------------------------------------------------------------------------
|
|
237
|
+
interrupt_flag: 0x00000002
|
|
238
|
+
interrupt_mask: 0x00000000
|
|
239
|
+
WARNING: Interrupts pending!
|
|
240
|
+
- TRAP
|
|
241
|
+
|
|
242
|
+
[4] Signal Information (from core dump):
|
|
243
|
+
--------------------------------------------------------------------------------
|
|
244
|
+
Program terminated with signal SIGSEGV, Segmentation fault.
|
|
245
|
+
...
|
|
246
|
+
~~~
|
|
247
|
+
|
|
248
|
+
This comprehensive overview helps quickly identify the root cause.
|
|
249
|
+
|
|
250
|
+
## Advanced Techniques
|
|
251
|
+
|
|
252
|
+
### Finding Fibers by Stack Address
|
|
253
|
+
|
|
254
|
+
If you know a stack address, find the owning fiber:
|
|
255
|
+
|
|
256
|
+
~~~
|
|
257
|
+
(gdb) info frame
|
|
258
|
+
... rsp = 0x7f8a1e000500 ...
|
|
259
|
+
|
|
260
|
+
(gdb) rb-fiber-from-stack 0x7f8a1e000000
|
|
261
|
+
Searching for fiber with stack base 0x7f8a1e000000...
|
|
262
|
+
|
|
263
|
+
Found fiber: 0x7f8a1c800300
|
|
264
|
+
Status: SUSPENDED
|
|
265
|
+
Stack: base=0x7f8a1e000000, size=1048576
|
|
266
|
+
~~~
|
|
267
|
+
|
|
268
|
+
### Searching Fibers by Function
|
|
269
|
+
|
|
270
|
+
Find which fibers are blocked in a specific C function:
|
|
271
|
+
|
|
272
|
+
~~~
|
|
273
|
+
(gdb) rb-fiber-c-stack-search pthread_cond_wait
|
|
274
|
+
Scanning 12 fiber(s)...
|
|
275
|
+
|
|
276
|
+
Match: Fiber #3 - found at 0x7f8a1e000450
|
|
277
|
+
Match: Fiber #7 - found at 0x7f8a1e100780
|
|
278
|
+
|
|
279
|
+
Search complete: 2 fiber(s) matched.
|
|
280
|
+
~~~
|
|
281
|
+
|
|
282
|
+
Use this to find all fibers waiting on locks or I/O.
|
|
283
|
+
|
|
284
|
+
### Debug Unwinder Issues
|
|
285
|
+
|
|
286
|
+
If fiber switching doesn't work as expected:
|
|
287
|
+
|
|
288
|
+
~~~
|
|
289
|
+
(gdb) rb-fiber-debug-unwind 5
|
|
290
|
+
Debug unwinding for Fiber #5: 0x7f8a1c800500
|
|
291
|
+
|
|
292
|
+
Coroutine context:
|
|
293
|
+
fiber->context.stack_pointer = 0x7f8a1e000480
|
|
294
|
+
|
|
295
|
+
Saved registers on coroutine stack:
|
|
296
|
+
[0x7f8a1e000480+0] = R15: 0x0000000000000000
|
|
297
|
+
[0x7f8a1e000480+8] = R14: 0x00007f8a1c567890
|
|
298
|
+
...
|
|
299
|
+
[0x7f8a1e000480+48] = RIP: 0x00007f8a1ab12345
|
|
300
|
+
|
|
301
|
+
Validation:
|
|
302
|
+
✓ RIP looks like a valid code address
|
|
303
|
+
Symbol: fiber_setcontext + 123
|
|
304
|
+
✓ RSP is within fiber's stack range
|
|
305
|
+
~~~
|
|
306
|
+
|
|
307
|
+
## Best Practices
|
|
308
|
+
|
|
309
|
+
### Scan Once, Use Indices
|
|
310
|
+
|
|
311
|
+
After scanning fibers, use indices for all operations:
|
|
312
|
+
|
|
313
|
+
~~~
|
|
314
|
+
(gdb) rb-scan-fibers # Scan once
|
|
315
|
+
(gdb) rb-fiber 5 # Use index thereafter
|
|
316
|
+
(gdb) rb-fiber-bt 5
|
|
317
|
+
(gdb) rb-fiber-switch 5
|
|
318
|
+
~~~
|
|
319
|
+
|
|
320
|
+
The cache persists throughout your GDB session.
|
|
321
|
+
|
|
322
|
+
### Check Fiber Status First
|
|
323
|
+
|
|
324
|
+
Before inspecting, check the fiber's status:
|
|
325
|
+
|
|
326
|
+
~~~
|
|
327
|
+
(gdb) rb-fiber 5
|
|
328
|
+
Status: TERMINATED # Won't have useful context
|
|
329
|
+
|
|
330
|
+
(gdb) rb-fiber 3
|
|
331
|
+
Status: SUSPENDED # Good candidate for inspection
|
|
332
|
+
~~~
|
|
333
|
+
|
|
334
|
+
CREATED and TERMINATED fibers may not have valid saved contexts.
|
|
335
|
+
|
|
336
|
+
### Use Convenience Variables
|
|
337
|
+
|
|
338
|
+
After switching to a fiber, use the set convenience variables:
|
|
339
|
+
|
|
340
|
+
~~~
|
|
341
|
+
(gdb) rb-fiber-switch 5
|
|
342
|
+
(gdb) rb-object-print $errinfo # Pre-set to fiber's exception
|
|
343
|
+
(gdb) rb-object-print $ec->storage # Fiber-local storage
|
|
344
|
+
~~~
|
|
345
|
+
|
|
346
|
+
## Common Pitfalls
|
|
347
|
+
|
|
348
|
+
### Fiber Not Suspended
|
|
349
|
+
|
|
350
|
+
Only SUSPENDED fibers have fully saved context:
|
|
351
|
+
|
|
352
|
+
~~~
|
|
353
|
+
Fiber status: CREATED (0)
|
|
354
|
+
Note: CREATED fibers may not have been suspended yet
|
|
355
|
+
~~~
|
|
356
|
+
|
|
357
|
+
CREATED fibers haven't yielded yet, so they don't have saved register state.
|
|
358
|
+
|
|
359
|
+
### Core Dump Limitations
|
|
360
|
+
|
|
361
|
+
Core dumps capture memory but not CPU registers. Switching to the current fiber may show incomplete stacks. For complete debugging, use live process debugging when possible.
|
|
362
|
+
|
|
363
|
+
### macOS Code Signing
|
|
364
|
+
|
|
365
|
+
On macOS, GDB may not be able to attach to running Ruby processes due to code signing restrictions. Core dump analysis works without issues.
|
|
366
|
+
|
|
367
|
+
## See Also
|
|
368
|
+
|
|
369
|
+
- {ruby Ruby::GDB::object-inspection Object inspection} for examining fiber storage and exception objects
|
|
370
|
+
- {ruby Ruby::GDB::stack-inspection Stack inspection} for understanding VM frames
|
|
371
|
+
- {ruby Ruby::GDB::heap-debugging Heap debugging} for finding fiber objects
|
|
372
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to install and use Ruby GDB extensions for debugging Ruby programs and core dumps.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem:
|
|
8
|
+
|
|
9
|
+
~~~ bash
|
|
10
|
+
$ gem install ruby-gdb
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
### Installing GDB Extensions
|
|
14
|
+
|
|
15
|
+
Install the extensions (automatically adds to `~/.gdbinit`):
|
|
16
|
+
|
|
17
|
+
~~~ bash
|
|
18
|
+
$ bake ruby:gdb:install
|
|
19
|
+
~~~
|
|
20
|
+
|
|
21
|
+
This adds a single line to your `~/.gdbinit` that sources the extensions from the gem's data directory. The extensions will then load automatically every time you start GDB.
|
|
22
|
+
|
|
23
|
+
To install to a custom `.gdbinit` location:
|
|
24
|
+
|
|
25
|
+
~~~ bash
|
|
26
|
+
$ bake ruby:gdb:install gdbinit=/path/to/custom/gdbinit
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
### Verifying Installation
|
|
30
|
+
|
|
31
|
+
Check installation status:
|
|
32
|
+
|
|
33
|
+
~~~ bash
|
|
34
|
+
$ bake ruby:gdb:info
|
|
35
|
+
Ruby GDB Extensions v0.1.0
|
|
36
|
+
Status: ✓ Installed
|
|
37
|
+
~~~
|
|
38
|
+
|
|
39
|
+
Test that extensions load automatically:
|
|
40
|
+
|
|
41
|
+
~~~ bash
|
|
42
|
+
$ gdb --batch -ex "help rb-object-print"
|
|
43
|
+
Recursively print Ruby hash and array structures...
|
|
44
|
+
~~~
|
|
45
|
+
|
|
46
|
+
### Uninstalling
|
|
47
|
+
|
|
48
|
+
To remove the extensions:
|
|
49
|
+
|
|
50
|
+
~~~ bash
|
|
51
|
+
$ bake ruby:gdb:uninstall
|
|
52
|
+
~~~
|
|
53
|
+
|
|
54
|
+
This removes the source line from your `~/.gdbinit`.
|
|
55
|
+
|
|
56
|
+
## Core Concepts
|
|
57
|
+
|
|
58
|
+
Ruby GDB provides specialized commands for debugging Ruby at multiple levels:
|
|
59
|
+
|
|
60
|
+
- **Object Inspection** - View Ruby objects, hashes, arrays, and structs with {ruby Ruby::GDB::object-inspection proper formatting}
|
|
61
|
+
- **Fiber Debugging** - Navigate and inspect fiber state, backtraces, and exceptions
|
|
62
|
+
- **Stack Analysis** - Examine both VM (Ruby) and C (native) stack frames
|
|
63
|
+
- **Heap Navigation** - Scan the Ruby heap to find objects and understand memory usage
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
### Debugging a Running Process
|
|
68
|
+
|
|
69
|
+
Start your Ruby program under GDB:
|
|
70
|
+
|
|
71
|
+
~~~ bash
|
|
72
|
+
$ gdb --args ruby my_script.rb
|
|
73
|
+
~~~
|
|
74
|
+
|
|
75
|
+
Set a breakpoint and run:
|
|
76
|
+
|
|
77
|
+
~~~
|
|
78
|
+
(gdb) break rb_vm_exec
|
|
79
|
+
(gdb) run
|
|
80
|
+
~~~
|
|
81
|
+
|
|
82
|
+
Once stopped, use Ruby debugging commands:
|
|
83
|
+
|
|
84
|
+
~~~
|
|
85
|
+
(gdb) rb-object-print $ec->cfp->sp[-1] # Print top of VM stack
|
|
86
|
+
(gdb) rb-scan-fibers # List all fibers
|
|
87
|
+
(gdb) rb-fiber-bt 0 # Show fiber backtrace
|
|
88
|
+
~~~
|
|
89
|
+
|
|
90
|
+
### Debugging a Core Dump
|
|
91
|
+
|
|
92
|
+
When your Ruby program crashes, you can analyze the core dump:
|
|
93
|
+
|
|
94
|
+
~~~ bash
|
|
95
|
+
$ gdb ruby core.dump
|
|
96
|
+
~~~
|
|
97
|
+
|
|
98
|
+
Load the Ruby extensions and diagnose the issue:
|
|
99
|
+
|
|
100
|
+
~~~
|
|
101
|
+
(gdb) source ~/.local/share/gdb/ruby/init.gdb
|
|
102
|
+
(gdb) rb-diagnose-exit # Check for exceptions and signals
|
|
103
|
+
(gdb) rb-scan-fibers # List all fibers
|
|
104
|
+
(gdb) rb-fiber-bt 0 # Show fiber backtraces
|
|
105
|
+
(gdb) rb-object-print $errinfo # Print exception objects
|
|
106
|
+
~~~
|
|
107
|
+
|
|
108
|
+
## Common Workflows
|
|
109
|
+
|
|
110
|
+
### Inspecting Exception Objects
|
|
111
|
+
|
|
112
|
+
When a Ruby exception occurs, you can inspect it in detail:
|
|
113
|
+
|
|
114
|
+
~~~
|
|
115
|
+
(gdb) break rb_exc_raise
|
|
116
|
+
(gdb) run
|
|
117
|
+
(gdb) rb-object-print $ec->errinfo --depth 2
|
|
118
|
+
~~~
|
|
119
|
+
|
|
120
|
+
This shows the exception class, message, and any nested structures.
|
|
121
|
+
|
|
122
|
+
### Debugging Fiber Issues
|
|
123
|
+
|
|
124
|
+
When working with fibers, you often need to see what each fiber is doing:
|
|
125
|
+
|
|
126
|
+
~~~
|
|
127
|
+
(gdb) rb-scan-fibers # Scan heap for all fibers
|
|
128
|
+
(gdb) rb-all-fiber-bt # Show backtraces for all fibers
|
|
129
|
+
(gdb) rb-fiber 5 # Inspect specific fiber
|
|
130
|
+
(gdb) rb-fiber-switch 5 # Switch GDB to fiber's stack
|
|
131
|
+
(gdb) bt # Now shows fiber's C backtrace
|
|
132
|
+
~~~
|
|
133
|
+
|
|
134
|
+
### Examining Complex Data Structures
|
|
135
|
+
|
|
136
|
+
Ruby hashes and arrays can contain nested structures:
|
|
137
|
+
|
|
138
|
+
~~~
|
|
139
|
+
(gdb) rb-object-print $some_hash --depth 3
|
|
140
|
+
(gdb) rb-object-print $ec->storage --depth 2
|
|
141
|
+
~~~
|
|
142
|
+
|
|
143
|
+
The `--depth` flag controls how deep to recurse into nested objects.
|
|
144
|
+
|
|
145
|
+
## Next Steps
|
|
146
|
+
|
|
147
|
+
- Learn about {ruby Ruby::GDB::object-inspection inspecting Ruby objects} in detail
|
|
148
|
+
- Explore {ruby Ruby::GDB::fiber-debugging fiber debugging} capabilities
|
|
149
|
+
- Understand {ruby Ruby::GDB::stack-inspection stack analysis} techniques
|
|
150
|
+
- Master {ruby Ruby::GDB::heap-debugging heap navigation} for memory issues
|
|
151
|
+
|
|
152
|
+
## Requirements
|
|
153
|
+
|
|
154
|
+
- GDB with Python support (GDB 7.0+)
|
|
155
|
+
- Ruby 3.0+ recommended (works with 2.x with limitations)
|
|
156
|
+
- For best results: Ruby built with debug symbols (`--with-debug-symbols` or install `ruby-debug` package)
|
|
157
|
+
|
|
158
|
+
## Platform Support
|
|
159
|
+
|
|
160
|
+
These extensions work on:
|
|
161
|
+
|
|
162
|
+
- **Linux**: Full support with all features
|
|
163
|
+
- **macOS**: Viewing core dumps works; running processes may require code signing or disabling SIP
|
|
164
|
+
- **BSD**: Should work similar to Linux (untested)
|
|
165
|
+
|
|
166
|
+
For production debugging, Linux with Ruby debug symbols provides the best experience.
|
|
167
|
+
|