toolbox 0.1.4 → 0.2.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
- checksums.yaml.gz.sig +0 -0
- data/bake/ruby/gdb.rb +135 -0
- data/bake/toolbox/gdb.rb +137 -0
- data/bake/toolbox/lldb.rb +137 -0
- data/context/fiber-debugging.md +171 -0
- data/context/getting-started.md +178 -0
- data/context/heap-debugging.md +351 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +208 -0
- data/context/stack-inspection.md +188 -0
- data/data/toolbox/command.py +254 -0
- data/data/toolbox/constants.py +200 -0
- data/data/toolbox/context.py +295 -0
- data/data/toolbox/debugger/__init__.py +99 -0
- data/data/toolbox/debugger/gdb_backend.py +595 -0
- data/data/toolbox/debugger/lldb_backend.py +885 -0
- data/data/toolbox/fiber.py +885 -0
- data/data/toolbox/format.py +200 -0
- data/data/toolbox/heap.py +669 -0
- data/data/toolbox/init.py +85 -0
- data/data/toolbox/object.py +84 -0
- data/data/toolbox/rarray.py +124 -0
- data/data/toolbox/rbasic.py +103 -0
- data/data/toolbox/rbignum.py +52 -0
- data/data/toolbox/rclass.py +136 -0
- data/data/toolbox/readme.md +214 -0
- data/data/toolbox/rexception.py +150 -0
- data/data/toolbox/rfloat.py +98 -0
- data/data/toolbox/rhash.py +159 -0
- data/data/toolbox/rstring.py +234 -0
- data/data/toolbox/rstruct.py +157 -0
- data/data/toolbox/rsymbol.py +302 -0
- data/data/toolbox/stack.py +630 -0
- data/data/toolbox/value.py +183 -0
- data/lib/toolbox/gdb.rb +21 -0
- data/lib/toolbox/lldb.rb +21 -0
- data/lib/toolbox/version.rb +7 -1
- data/lib/toolbox.rb +9 -24
- data/license.md +21 -0
- data/readme.md +64 -0
- data/releases.md +9 -0
- data.tar.gz.sig +2 -0
- metadata +95 -165
- metadata.gz.sig +0 -0
- data/Rakefile +0 -61
- data/lib/dirs.rb +0 -9
- data/lib/toolbox/config.rb +0 -211
- data/lib/toolbox/default_controller.rb +0 -393
- data/lib/toolbox/helpers.rb +0 -11
- data/lib/toolbox/rendering.rb +0 -413
- data/lib/toolbox/searching.rb +0 -85
- data/lib/toolbox/session_params.rb +0 -63
- data/lib/toolbox/sorting.rb +0 -74
- data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
- data/public/images/add.png +0 -0
- data/public/images/arrow_down.gif +0 -0
- data/public/images/arrow_up.gif +0 -0
- data/public/images/close.png +0 -0
- data/public/images/edit.gif +0 -0
- data/public/images/email.png +0 -0
- data/public/images/page.png +0 -0
- data/public/images/page_acrobat.png +0 -0
- data/public/images/page_add.png +0 -0
- data/public/images/page_copy.png +0 -0
- data/public/images/page_delete.png +0 -0
- data/public/images/page_edit.png +0 -0
- data/public/images/page_excel.png +0 -0
- data/public/images/page_list.png +0 -0
- data/public/images/page_save.png +0 -0
- data/public/images/page_word.png +0 -0
- data/public/images/remove.png +0 -0
- data/public/images/show.gif +0 -0
- data/public/images/spinner.gif +0 -0
- data/public/javascripts/popup.js +0 -498
- data/public/javascripts/toolbox.js +0 -18
- data/public/stylesheets/context_menu.css +0 -168
- data/public/stylesheets/popup.css +0 -30
- data/public/stylesheets/toolbox.css +0 -107
- data/view/toolbox/_collection.html.erb +0 -24
- data/view/toolbox/_collection_header.html.erb +0 -7
- data/view/toolbox/_context_menu.html.erb +0 -17
- data/view/toolbox/_dialogs.html.erb +0 -6
- data/view/toolbox/_form.html.erb +0 -30
- data/view/toolbox/_form_collection_row.html.erb +0 -18
- data/view/toolbox/_form_fieldset.html.erb +0 -30
- data/view/toolbox/_form_fieldset_row.html.erb +0 -19
- data/view/toolbox/_list.html.erb +0 -25
- data/view/toolbox/_list_row.html.erb +0 -10
- data/view/toolbox/_menu.html.erb +0 -7
- data/view/toolbox/_search_field.html.erb +0 -8
- data/view/toolbox/_show.html.erb +0 -12
- data/view/toolbox/_show_collection_row.html.erb +0 -6
- data/view/toolbox/_show_fieldset.html.erb +0 -21
- data/view/toolbox/edit.html.erb +0 -5
- data/view/toolbox/index.html.erb +0 -3
- data/view/toolbox/new.html.erb +0 -9
- data/view/toolbox/show.html.erb +0 -39
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Object Inspection
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `rb-object-print` to inspect Ruby objects, hashes, arrays, and structs in GDB.
|
|
4
|
+
|
|
5
|
+
## Why Object Inspection Matters
|
|
6
|
+
|
|
7
|
+
When debugging Ruby programs or analyzing core dumps, you often need to inspect complex data structures that are difficult to read in their raw memory representation. Standard GDB commands show pointer addresses and raw memory, but not the logical structure of Ruby objects.
|
|
8
|
+
|
|
9
|
+
Use `rb-object-print` when you need:
|
|
10
|
+
|
|
11
|
+
- **Understand exception objects**: See the full exception hierarchy, message, and backtrace data
|
|
12
|
+
- **Inspect fiber storage**: View thread-local data and fiber-specific variables
|
|
13
|
+
- **Debug data corruption**: Check the contents of hashes and arrays for unexpected values
|
|
14
|
+
- **Analyze VM state**: Examine objects on the VM stack without manual pointer arithmetic
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
The `rb-object-print` command recursively prints Ruby objects in a human-readable format.
|
|
19
|
+
|
|
20
|
+
### Syntax
|
|
21
|
+
|
|
22
|
+
~~~
|
|
23
|
+
rb-object-print <expression> [--depth N] [--debug]
|
|
24
|
+
~~~
|
|
25
|
+
|
|
26
|
+
Where:
|
|
27
|
+
- `<expression>`: Any GDB expression that evaluates to a Ruby VALUE
|
|
28
|
+
- `--depth N`: Maximum recursion depth (default: 1)
|
|
29
|
+
- `--debug`: Enable diagnostic output for troubleshooting
|
|
30
|
+
|
|
31
|
+
### Simple Values
|
|
32
|
+
|
|
33
|
+
Print immediate values and special constants:
|
|
34
|
+
|
|
35
|
+
~~~
|
|
36
|
+
(gdb) rb-object-print 0 # <T_FALSE>
|
|
37
|
+
(gdb) rb-object-print 8 # <T_NIL>
|
|
38
|
+
(gdb) rb-object-print 20 # <T_TRUE>
|
|
39
|
+
(gdb) rb-object-print 85 # <T_FIXNUM> 42
|
|
40
|
+
~~~
|
|
41
|
+
|
|
42
|
+
These work without any Ruby process running, making them useful for learning and testing.
|
|
43
|
+
|
|
44
|
+
### Expressions
|
|
45
|
+
|
|
46
|
+
Use any valid GDB expression:
|
|
47
|
+
|
|
48
|
+
~~~
|
|
49
|
+
(gdb) rb-object-print $ec->errinfo # Exception object
|
|
50
|
+
(gdb) rb-object-print $ec->cfp->sp[-1] # Top of VM stack
|
|
51
|
+
(gdb) rb-object-print $ec->storage # Fiber storage hash
|
|
52
|
+
(gdb) rb-object-print (VALUE)0x00007f8a12345678 # Object at specific address
|
|
53
|
+
~~~
|
|
54
|
+
|
|
55
|
+
## Inspecting Hashes
|
|
56
|
+
|
|
57
|
+
Ruby hashes have two internal representations (ST table and AR table). The command automatically detects and displays both:
|
|
58
|
+
|
|
59
|
+
### Small Hashes (AR Table)
|
|
60
|
+
|
|
61
|
+
For hashes with fewer than 8 entries, Ruby uses an array-based implementation:
|
|
62
|
+
|
|
63
|
+
~~~
|
|
64
|
+
(gdb) rb-object-print $some_hash
|
|
65
|
+
<T_HASH@...>
|
|
66
|
+
[ 0] K: <T_SYMBOL> :name
|
|
67
|
+
V: <T_STRING@...> "Alice"
|
|
68
|
+
[ 1] K: <T_SYMBOL> :age
|
|
69
|
+
V: <T_FIXNUM> 30
|
|
70
|
+
[ 2] K: <T_SYMBOL> :active
|
|
71
|
+
V: <T_TRUE>
|
|
72
|
+
~~~
|
|
73
|
+
|
|
74
|
+
### Large Hashes (ST Table)
|
|
75
|
+
|
|
76
|
+
For larger hashes, Ruby uses a hash table (output format is similar):
|
|
77
|
+
|
|
78
|
+
~~~
|
|
79
|
+
(gdb) rb-object-print $large_hash
|
|
80
|
+
<T_HASH@...>
|
|
81
|
+
[ 0] K: <T_SYMBOL> :user_id
|
|
82
|
+
V: <T_FIXNUM> 12345
|
|
83
|
+
[ 1] K: <T_SYMBOL> :session_data
|
|
84
|
+
V: <T_STRING@...> "..."
|
|
85
|
+
...
|
|
86
|
+
~~~
|
|
87
|
+
|
|
88
|
+
### Controlling Depth
|
|
89
|
+
|
|
90
|
+
Prevent overwhelming output from deeply nested structures:
|
|
91
|
+
|
|
92
|
+
~~~
|
|
93
|
+
(gdb) rb-object-print $nested_hash --depth 1 # Only top level
|
|
94
|
+
<T_HASH@...>
|
|
95
|
+
[ 0] K: <T_SYMBOL> :data
|
|
96
|
+
V: <T_HASH@...> # Nested hash not expanded
|
|
97
|
+
|
|
98
|
+
(gdb) rb-object-print $nested_hash --depth 2 # Expand one level
|
|
99
|
+
<T_HASH@...>
|
|
100
|
+
[ 0] K: <T_SYMBOL> :data
|
|
101
|
+
V: <T_HASH@...>
|
|
102
|
+
[ 0] K: <T_SYMBOL> :nested_key
|
|
103
|
+
V: <T_STRING@...> "value"
|
|
104
|
+
~~~
|
|
105
|
+
|
|
106
|
+
At depth 1, nested structures show their type and address but aren't expanded. Increase depth to expand them.
|
|
107
|
+
|
|
108
|
+
## Inspecting Arrays
|
|
109
|
+
|
|
110
|
+
Arrays also have two representations based on size:
|
|
111
|
+
|
|
112
|
+
### Arrays
|
|
113
|
+
|
|
114
|
+
Arrays display their elements with type information:
|
|
115
|
+
|
|
116
|
+
~~~
|
|
117
|
+
(gdb) rb-object-print $array
|
|
118
|
+
<T_ARRAY@...>
|
|
119
|
+
[ 0] <T_FIXNUM> 1
|
|
120
|
+
[ 1] <T_FIXNUM> 2
|
|
121
|
+
[ 2] <T_FIXNUM> 3
|
|
122
|
+
~~~
|
|
123
|
+
|
|
124
|
+
For arrays with nested objects:
|
|
125
|
+
|
|
126
|
+
~~~
|
|
127
|
+
(gdb) rb-object-print $array --depth 2
|
|
128
|
+
<T_ARRAY@...>
|
|
129
|
+
[ 0] <T_STRING@...> "first item"
|
|
130
|
+
[ 1] <T_HASH@...>
|
|
131
|
+
[ 0] K: <T_SYMBOL> :key
|
|
132
|
+
V: <T_FIXNUM> 123
|
|
133
|
+
...
|
|
134
|
+
~~~
|
|
135
|
+
|
|
136
|
+
## Inspecting Structs
|
|
137
|
+
|
|
138
|
+
Ruby Struct objects work similarly to arrays:
|
|
139
|
+
|
|
140
|
+
~~~
|
|
141
|
+
(gdb) rb-object-print $struct_instance
|
|
142
|
+
<T_STRUCT@...>
|
|
143
|
+
[ 0] <T_STRING@...> "John"
|
|
144
|
+
[ 1] <T_FIXNUM> 25
|
|
145
|
+
[ 2] <T_STRING@...> "Engineer"
|
|
146
|
+
[ 3] <T_TRUE>
|
|
147
|
+
~~~
|
|
148
|
+
|
|
149
|
+
## Practical Examples
|
|
150
|
+
|
|
151
|
+
### Debugging Exception in Fiber
|
|
152
|
+
|
|
153
|
+
When a fiber has an exception, inspect it:
|
|
154
|
+
|
|
155
|
+
~~~
|
|
156
|
+
(gdb) rb-fiber-scan-heap
|
|
157
|
+
(gdb) rb-fiber-scan-switch 5 # Switch to fiber #5
|
|
158
|
+
(gdb) rb-object-print $errinfo --depth 3
|
|
159
|
+
~~~
|
|
160
|
+
|
|
161
|
+
This reveals the full exception structure including any nested causes. After switching to a fiber, `$errinfo` and `$ec` convenience variables are automatically set.
|
|
162
|
+
|
|
163
|
+
### Inspecting Method Arguments
|
|
164
|
+
|
|
165
|
+
Break at a method and examine arguments on the stack:
|
|
166
|
+
|
|
167
|
+
~~~
|
|
168
|
+
(gdb) break some_method
|
|
169
|
+
(gdb) run
|
|
170
|
+
(gdb) rb-object-print $ec->cfp->sp[-1] # Last argument
|
|
171
|
+
(gdb) rb-object-print $ec->cfp->sp[-2] # Second-to-last argument
|
|
172
|
+
~~~
|
|
173
|
+
|
|
174
|
+
### Examining Fiber Storage
|
|
175
|
+
|
|
176
|
+
Thread-local variables are stored in fiber storage:
|
|
177
|
+
|
|
178
|
+
~~~
|
|
179
|
+
(gdb) rb-fiber-scan-heap
|
|
180
|
+
(gdb) rb-fiber-scan-switch 0
|
|
181
|
+
(gdb) rb-object-print $ec->storage --depth 2
|
|
182
|
+
~~~
|
|
183
|
+
|
|
184
|
+
This shows all thread-local variables and their values.
|
|
185
|
+
|
|
186
|
+
## Debugging with --debug Flag
|
|
187
|
+
|
|
188
|
+
When `rb-object-print` doesn't show what you expect, use `--debug`:
|
|
189
|
+
|
|
190
|
+
~~~
|
|
191
|
+
(gdb) rb-object-print $suspicious_value --debug
|
|
192
|
+
DEBUG: Evaluated '$suspicious_value' to 0x7f8a1c567890
|
|
193
|
+
DEBUG: Loaded constant RUBY_T_MASK = 31
|
|
194
|
+
DEBUG: Object at 0x7f8a1c567890 with flags=0x20040005, type=0x5
|
|
195
|
+
...
|
|
196
|
+
~~~
|
|
197
|
+
|
|
198
|
+
This shows:
|
|
199
|
+
- How the expression was evaluated
|
|
200
|
+
- What constants were loaded
|
|
201
|
+
- Object type detection logic
|
|
202
|
+
- Any errors encountered
|
|
203
|
+
|
|
204
|
+
Use this to troubleshoot:
|
|
205
|
+
- Unexpected output format
|
|
206
|
+
- Missing nested structures
|
|
207
|
+
- Type detection issues
|
|
208
|
+
- Access errors
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Stack Inspection
|
|
2
|
+
|
|
3
|
+
This guide explains how to inspect both Ruby VM stacks and native C stacks when debugging Ruby programs.
|
|
4
|
+
|
|
5
|
+
## Understanding Ruby's Dual Stack System
|
|
6
|
+
|
|
7
|
+
Ruby programs operate with two distinct stacks that serve different purposes. Understanding both is crucial for effective debugging, especially when tracking down segfaults, stack overflows, or unexpected behavior in C extensions.
|
|
8
|
+
|
|
9
|
+
Use stack inspection when you need:
|
|
10
|
+
|
|
11
|
+
- **Trace execution flow**: Understand the sequence of method calls that led to the current state
|
|
12
|
+
- **Debug C extensions**: See both Ruby and native frames when extensions are involved
|
|
13
|
+
- **Find stack overflows**: Identify deep recursion in either Ruby or C code
|
|
14
|
+
- **Understand fiber switches**: See where fibers yield and resume
|
|
15
|
+
|
|
16
|
+
## The Two Stack Types
|
|
17
|
+
|
|
18
|
+
### VM Stack (Ruby Level)
|
|
19
|
+
|
|
20
|
+
The VM stack holds:
|
|
21
|
+
- Ruby method call frames (control frames)
|
|
22
|
+
- Local variables and temporaries
|
|
23
|
+
- Method arguments
|
|
24
|
+
- Block parameters
|
|
25
|
+
- Return values
|
|
26
|
+
|
|
27
|
+
This is what you see with Ruby's `caller` method at runtime.
|
|
28
|
+
|
|
29
|
+
### C Stack (Native Level)
|
|
30
|
+
|
|
31
|
+
The C/machine stack holds:
|
|
32
|
+
- Native function call frames
|
|
33
|
+
- C local variables
|
|
34
|
+
- Saved registers
|
|
35
|
+
- Return addresses
|
|
36
|
+
|
|
37
|
+
This is what GDB's `bt` command shows by default.
|
|
38
|
+
|
|
39
|
+
## Inspecting VM Stacks
|
|
40
|
+
|
|
41
|
+
### Current Frame Information
|
|
42
|
+
|
|
43
|
+
See the current Ruby control frame:
|
|
44
|
+
|
|
45
|
+
~~~
|
|
46
|
+
(gdb) set $ec = ruby_current_execution_context_ptr
|
|
47
|
+
(gdb) set $cfp = $ec->cfp
|
|
48
|
+
(gdb) p $cfp->pc # Program counter
|
|
49
|
+
(gdb) p $cfp->sp # Stack pointer
|
|
50
|
+
(gdb) p $cfp->iseq # Instruction sequence
|
|
51
|
+
(gdb) p $cfp->ep # Environment pointer
|
|
52
|
+
~~~
|
|
53
|
+
|
|
54
|
+
### Combined Stack Trace
|
|
55
|
+
|
|
56
|
+
The simplest way to see both Ruby and C frames:
|
|
57
|
+
|
|
58
|
+
~~~
|
|
59
|
+
(gdb) rb-fiber-scan-heap
|
|
60
|
+
(gdb) rb-fiber-scan-switch 5
|
|
61
|
+
(gdb) rb-stack-trace
|
|
62
|
+
Combined Ruby/C backtrace for Fiber #5:
|
|
63
|
+
[C] fiber_setcontext
|
|
64
|
+
[R] /app/lib/connection.rb:123:in `read'
|
|
65
|
+
[C] rb_io_wait_readable
|
|
66
|
+
[R] /app/lib/connection.rb:89:in `receive'
|
|
67
|
+
...
|
|
68
|
+
~~~
|
|
69
|
+
|
|
70
|
+
This shows both levels of the call stack in order.
|
|
71
|
+
|
|
72
|
+
### Detailed Frame Information
|
|
73
|
+
|
|
74
|
+
For advanced debugging, you can inspect the raw VM frames:
|
|
75
|
+
|
|
76
|
+
~~~
|
|
77
|
+
(gdb) set $ec = ruby_current_execution_context_ptr
|
|
78
|
+
(gdb) set $cfp = $ec->cfp
|
|
79
|
+
|
|
80
|
+
# Current frame details:
|
|
81
|
+
(gdb) p $cfp->pc # Program counter
|
|
82
|
+
(gdb) p $cfp->sp # Stack pointer
|
|
83
|
+
(gdb) p $cfp->iseq # Instruction sequence
|
|
84
|
+
(gdb) p $cfp->ep # Environment pointer
|
|
85
|
+
~~~
|
|
86
|
+
|
|
87
|
+
## Inspecting C Stacks
|
|
88
|
+
|
|
89
|
+
### C Backtrace with GDB
|
|
90
|
+
|
|
91
|
+
After switching to a fiber, use standard GDB commands to inspect the C stack:
|
|
92
|
+
|
|
93
|
+
~~~
|
|
94
|
+
(gdb) rb-fiber-scan-switch 5
|
|
95
|
+
(gdb) bt # Show C backtrace
|
|
96
|
+
(gdb) frame 2 # Switch to specific frame
|
|
97
|
+
(gdb) info args # Show function arguments
|
|
98
|
+
(gdb) info locals # Show local variables
|
|
99
|
+
~~~
|
|
100
|
+
|
|
101
|
+
The fiber unwinder automatically integrates with GDB's backtrace functionality, so `bt` shows the correct C stack for the selected fiber.
|
|
102
|
+
|
|
103
|
+
## Practical Examples
|
|
104
|
+
|
|
105
|
+
### Finding Where Execution Stopped
|
|
106
|
+
|
|
107
|
+
Identify the exact location in both Ruby and C:
|
|
108
|
+
|
|
109
|
+
~~~
|
|
110
|
+
(gdb) rb-fiber-scan-heap
|
|
111
|
+
(gdb) rb-fiber-scan-switch 5 # Switch to fiber context
|
|
112
|
+
(gdb) rb-stack-trace # Combined backtrace
|
|
113
|
+
[R] /app/lib/connection.rb:123:in `read'
|
|
114
|
+
[C] rb_fiber_yield
|
|
115
|
+
[C] rb_io_wait_readable
|
|
116
|
+
[R] /app/lib/connection.rb:89:in `receive'
|
|
117
|
+
~~~
|
|
118
|
+
|
|
119
|
+
This shows the fiber is suspended in `read`, waiting for I/O.
|
|
120
|
+
|
|
121
|
+
### Debugging Deep Recursion
|
|
122
|
+
|
|
123
|
+
Detect excessive call depth:
|
|
124
|
+
|
|
125
|
+
~~~
|
|
126
|
+
(gdb) rb-fiber-scan-heap
|
|
127
|
+
(gdb) rb-fiber-scan-switch 5
|
|
128
|
+
(gdb) rb-stack-trace | grep "/app/lib/parser.rb:45" | wc -l
|
|
129
|
+
134 # Same line appearing 134 times!
|
|
130
|
+
~~~
|
|
131
|
+
|
|
132
|
+
Identifies a recursion issue in the parser.
|
|
133
|
+
|
|
134
|
+
### Examining Stack Values
|
|
135
|
+
|
|
136
|
+
See what values are on the current frame's stack:
|
|
137
|
+
|
|
138
|
+
~~~
|
|
139
|
+
(gdb) rb-fiber-scan-switch 5
|
|
140
|
+
(gdb) set $sp = $ec->cfp->sp
|
|
141
|
+
|
|
142
|
+
# Print values on stack
|
|
143
|
+
(gdb) rb-object-print *(VALUE*)($sp - 1) # Top of stack
|
|
144
|
+
(gdb) rb-object-print *(VALUE*)($sp - 2) # Second value
|
|
145
|
+
(gdb) rb-object-print *(VALUE*)($sp - 3) # Third value
|
|
146
|
+
~~~
|
|
147
|
+
|
|
148
|
+
### Tracking Fiber Switches
|
|
149
|
+
|
|
150
|
+
See the call stack across fiber boundaries:
|
|
151
|
+
|
|
152
|
+
~~~
|
|
153
|
+
(gdb) rb-fiber-scan-switch 5
|
|
154
|
+
(gdb) bt
|
|
155
|
+
#0 fiber_setcontext
|
|
156
|
+
#1 rb_fiber_yield # Fiber yielded here
|
|
157
|
+
#2 rb_io_wait_readable # Waiting for I/O
|
|
158
|
+
#3 some_io_operation
|
|
159
|
+
|
|
160
|
+
(gdb) frame 3
|
|
161
|
+
(gdb) info locals # C variables at I/O call
|
|
162
|
+
~~~
|
|
163
|
+
|
|
164
|
+
## Combining VM and C Stacks
|
|
165
|
+
|
|
166
|
+
For the complete picture, use the combined stack trace:
|
|
167
|
+
|
|
168
|
+
~~~
|
|
169
|
+
(gdb) rb-fiber-scan-switch 5
|
|
170
|
+
(gdb) rb-stack-trace
|
|
171
|
+
Combined Ruby/C backtrace:
|
|
172
|
+
[R] /app/lib/connection.rb:123:in `read'
|
|
173
|
+
[C] rb_io_wait_readable
|
|
174
|
+
[C] rb_io_read
|
|
175
|
+
[R] /app/lib/connection.rb:89:in `receive'
|
|
176
|
+
[C] rb_fiber_yield
|
|
177
|
+
[R] /app/lib/server.rb:56:in `handle_client'
|
|
178
|
+
...
|
|
179
|
+
~~~
|
|
180
|
+
|
|
181
|
+
Or separately:
|
|
182
|
+
|
|
183
|
+
~~~
|
|
184
|
+
(gdb) rb-stack-trace --values # Ruby perspective with stack values
|
|
185
|
+
(gdb) bt # C perspective only
|
|
186
|
+
~~~
|
|
187
|
+
|
|
188
|
+
The combined view reveals the full execution path through both Ruby and C code.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
class Arguments:
|
|
2
|
+
"""Structured result from parsing GDB command arguments.
|
|
3
|
+
|
|
4
|
+
Attributes:
|
|
5
|
+
expressions: List of expressions to evaluate (e.g., ["$var", "$ec->cfp->sp[-1]"])
|
|
6
|
+
flags: Set of boolean flags (e.g., {'debug'})
|
|
7
|
+
options: Dict of options with values (e.g., {'depth': 3})
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, expressions, flags, options):
|
|
10
|
+
self.expressions = expressions
|
|
11
|
+
self.flags = flags
|
|
12
|
+
self.options = options
|
|
13
|
+
|
|
14
|
+
def has_flag(self, flag_name):
|
|
15
|
+
"""Check if a boolean flag is present"""
|
|
16
|
+
return flag_name in self.flags
|
|
17
|
+
|
|
18
|
+
def get_option(self, option_name, default=None):
|
|
19
|
+
"""Get an option value with optional default"""
|
|
20
|
+
return self.options.get(option_name, default)
|
|
21
|
+
|
|
22
|
+
class ArgumentParser:
|
|
23
|
+
"""Parse GDB command arguments handling nested brackets, quotes, and flags.
|
|
24
|
+
|
|
25
|
+
This parser correctly handles:
|
|
26
|
+
- Nested brackets: $ec->cfp->sp[-1]
|
|
27
|
+
- Nested parentheses: ((struct foo*)bar)->baz
|
|
28
|
+
- Quotes: "string value"
|
|
29
|
+
- Flags: --debug, --depth 3
|
|
30
|
+
- Complex expressions: foo + 10, bar->ptr[idx * 2]
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, argument_string):
|
|
34
|
+
self.argument_string = argument_string.strip()
|
|
35
|
+
self.position = 0
|
|
36
|
+
self.length = len(self.argument_string)
|
|
37
|
+
|
|
38
|
+
def parse(self):
|
|
39
|
+
"""Parse the argument string and return (expressions, flags, options)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
tuple: (expressions: list[str], flags: set, options: dict)
|
|
43
|
+
- expressions: List of expressions to evaluate
|
|
44
|
+
- flags: Set of boolean flags (e.g., {'debug'})
|
|
45
|
+
- options: Dict of options with values (e.g., {'depth': 3})
|
|
46
|
+
"""
|
|
47
|
+
flags = set()
|
|
48
|
+
options = {}
|
|
49
|
+
expressions = []
|
|
50
|
+
|
|
51
|
+
while self.position < self.length:
|
|
52
|
+
self.skip_whitespace()
|
|
53
|
+
if self.position >= self.length:
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if self.peek() == '-' and self.peek(1) == '-':
|
|
57
|
+
# Parse a flag or option
|
|
58
|
+
flag_name, flag_value = self.parse_flag()
|
|
59
|
+
if flag_value is None:
|
|
60
|
+
flags.add(flag_name)
|
|
61
|
+
else:
|
|
62
|
+
options[flag_name] = flag_value
|
|
63
|
+
else:
|
|
64
|
+
# Parse an expression
|
|
65
|
+
expression = self.parse_expression()
|
|
66
|
+
if expression:
|
|
67
|
+
expressions.append(expression)
|
|
68
|
+
|
|
69
|
+
return expressions, flags, options
|
|
70
|
+
|
|
71
|
+
def peek(self, offset=0):
|
|
72
|
+
"""Peek at character at current position + offset"""
|
|
73
|
+
position = self.position + offset
|
|
74
|
+
if position < self.length:
|
|
75
|
+
return self.argument_string[position]
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def consume(self, count=1):
|
|
79
|
+
"""Consume and return count characters"""
|
|
80
|
+
result = self.argument_string[self.position:self.position + count]
|
|
81
|
+
self.position += count
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def skip_whitespace(self):
|
|
85
|
+
"""Skip whitespace characters"""
|
|
86
|
+
while self.position < self.length and self.argument_string[self.position].isspace():
|
|
87
|
+
self.position += 1
|
|
88
|
+
|
|
89
|
+
def parse_flag(self):
|
|
90
|
+
"""Parse a flag starting with --
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
tuple: (flag_name: str, value: str|None)
|
|
94
|
+
- For boolean flags: ('debug', None)
|
|
95
|
+
- For valued flags: ('depth', '3')
|
|
96
|
+
"""
|
|
97
|
+
# Consume '--'
|
|
98
|
+
self.consume(2)
|
|
99
|
+
|
|
100
|
+
# Read flag name
|
|
101
|
+
flag_name = ''
|
|
102
|
+
while self.position < self.length and self.argument_string[self.position].isalnum():
|
|
103
|
+
flag_name += self.consume()
|
|
104
|
+
|
|
105
|
+
# Check if flag has a value
|
|
106
|
+
self.skip_whitespace()
|
|
107
|
+
|
|
108
|
+
# If next character is not another flag and not end of string, it might be a value
|
|
109
|
+
if self.position < self.length and not (self.peek() == '-' and self.peek(1) == '-'):
|
|
110
|
+
# Try to parse a value - it could start with $, digit, or letter
|
|
111
|
+
if self.peek() and not self.peek().isspace():
|
|
112
|
+
value = ''
|
|
113
|
+
while self.position < self.length and not self.argument_string[self.position].isspace():
|
|
114
|
+
if self.peek() == '-' and self.peek(1) == '-':
|
|
115
|
+
break
|
|
116
|
+
value += self.consume()
|
|
117
|
+
|
|
118
|
+
# Try to convert to int if it's a number
|
|
119
|
+
try:
|
|
120
|
+
return flag_name, int(value)
|
|
121
|
+
except ValueError:
|
|
122
|
+
return flag_name, value
|
|
123
|
+
|
|
124
|
+
return flag_name, None
|
|
125
|
+
|
|
126
|
+
def parse_expression(self):
|
|
127
|
+
"""Parse a single expression, stopping at whitespace (unless nested) or flags.
|
|
128
|
+
|
|
129
|
+
An expression can be:
|
|
130
|
+
- A quoted string: "foo" or 'bar'
|
|
131
|
+
- A parenthesized expression: (x + y)
|
|
132
|
+
- A variable with accessors: $ec->cfp->sp[-1]
|
|
133
|
+
- Any combination that doesn't contain unquoted/unnested whitespace
|
|
134
|
+
"""
|
|
135
|
+
expression = ''
|
|
136
|
+
|
|
137
|
+
while self.position < self.length:
|
|
138
|
+
self.skip_whitespace()
|
|
139
|
+
if self.position >= self.length:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
character = self.peek()
|
|
143
|
+
|
|
144
|
+
# Stop at flags
|
|
145
|
+
if character == '-' and self.peek(1) == '-':
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
# Handle quoted strings - these are complete expressions
|
|
149
|
+
if character in ('"', "'"):
|
|
150
|
+
quoted = self.parse_quoted_string(character)
|
|
151
|
+
expression += quoted
|
|
152
|
+
# After a quoted string, we're done with this expression
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# Handle parentheses - collect the whole balanced expression
|
|
156
|
+
if character == '(':
|
|
157
|
+
expression += self.parse_balanced('(', ')')
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# Handle brackets
|
|
161
|
+
if character == '[':
|
|
162
|
+
expression += self.parse_balanced('[', ']')
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Handle braces
|
|
166
|
+
if character == '{':
|
|
167
|
+
expression += self.parse_balanced('{', '}')
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Stop at whitespace (this separates expressions)
|
|
171
|
+
if character.isspace():
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
# Regular character - part of a variable name, operator, etc.
|
|
175
|
+
expression += self.consume()
|
|
176
|
+
|
|
177
|
+
return expression.strip()
|
|
178
|
+
|
|
179
|
+
def parse_quoted_string(self, quote_character):
|
|
180
|
+
"""Parse a quoted string, handling escapes"""
|
|
181
|
+
result = self.consume() # Opening quote
|
|
182
|
+
|
|
183
|
+
while self.position < self.length:
|
|
184
|
+
character = self.peek()
|
|
185
|
+
|
|
186
|
+
if character == '\\':
|
|
187
|
+
# Escape sequence
|
|
188
|
+
result += self.consume()
|
|
189
|
+
if self.position < self.length:
|
|
190
|
+
result += self.consume()
|
|
191
|
+
elif character == quote_character:
|
|
192
|
+
# Closing quote
|
|
193
|
+
result += self.consume()
|
|
194
|
+
break
|
|
195
|
+
else:
|
|
196
|
+
result += self.consume()
|
|
197
|
+
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
def parse_balanced(self, open_character, close_character):
|
|
201
|
+
"""Parse a balanced pair of delimiters (e.g., parentheses, brackets)"""
|
|
202
|
+
result = self.consume() # Opening delimiter
|
|
203
|
+
depth = 1
|
|
204
|
+
|
|
205
|
+
while self.position < self.length and depth > 0:
|
|
206
|
+
character = self.peek()
|
|
207
|
+
|
|
208
|
+
# Handle quotes inside balanced delimiters
|
|
209
|
+
if character in ('"', "'"):
|
|
210
|
+
result += self.parse_quoted_string(character)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if character == open_character:
|
|
214
|
+
depth += 1
|
|
215
|
+
elif character == close_character:
|
|
216
|
+
depth -= 1
|
|
217
|
+
|
|
218
|
+
result += self.consume()
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def parse_arguments(input):
|
|
223
|
+
"""Convenience function to parse argument string.
|
|
224
|
+
|
|
225
|
+
Arguments:
|
|
226
|
+
input: The raw argument string from GDB command
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Arguments: Structured object with expressions, flags, and options
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
>>> arguments = parse_arguments("$var --debug")
|
|
233
|
+
>>> arguments.expressions
|
|
234
|
+
['$var']
|
|
235
|
+
>>> arguments.has_flag('debug')
|
|
236
|
+
True
|
|
237
|
+
|
|
238
|
+
>>> arguments = parse_arguments('"foo" "bar" --depth 3')
|
|
239
|
+
>>> arguments.expressions
|
|
240
|
+
['"foo"', '"bar"']
|
|
241
|
+
>>> arguments.get_option('depth')
|
|
242
|
+
3
|
|
243
|
+
|
|
244
|
+
>>> arguments = parse_arguments("$ec->cfp->sp[-1] --debug --depth 2")
|
|
245
|
+
>>> arguments.expressions
|
|
246
|
+
['$ec->cfp->sp[-1]']
|
|
247
|
+
>>> arguments.has_flag('debug')
|
|
248
|
+
True
|
|
249
|
+
>>> arguments.get_option('depth')
|
|
250
|
+
2
|
|
251
|
+
"""
|
|
252
|
+
parser = ArgumentParser(input)
|
|
253
|
+
expressions, flags, options = parser.parse()
|
|
254
|
+
return Arguments(expressions, flags, options)
|