2017-05-27 17:04:31 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
#coding: utf-8
|
|
|
|
#
|
|
|
|
# Usage: run `command script import -r misc/lldb_cruby.py` on LLDB
|
|
|
|
#
|
2017-08-03 09:39:25 +00:00
|
|
|
# Test: misc/test_lldb_cruby.rb
|
|
|
|
#
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2019-09-24 21:05:29 +09:00
|
|
|
from __future__ import print_function
|
2017-05-27 17:04:31 +00:00
|
|
|
import lldb
|
|
|
|
import os
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
import inspect
|
|
|
|
import sys
|
2017-05-27 17:04:31 +00:00
|
|
|
import shlex
|
2022-01-26 20:16:33 +00:00
|
|
|
import platform
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
import glob
|
2025-04-08 12:52:49 -04:00
|
|
|
import math
|
2022-01-26 20:16:33 +00:00
|
|
|
|
2022-07-13 18:14:44 +01:00
|
|
|
from lldb_rb.constants import *
|
2020-09-02 16:42:56 -07:00
|
|
|
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
# BEGIN FUNCTION STYLE DECLS
|
|
|
|
# This will be refactored to use class style decls in the misc/commands dir
|
2020-10-14 16:41:07 -07:00
|
|
|
class BackTrace:
|
|
|
|
VM_FRAME_MAGIC_METHOD = 0x11110001
|
|
|
|
VM_FRAME_MAGIC_BLOCK = 0x22220001
|
|
|
|
VM_FRAME_MAGIC_CLASS = 0x33330001
|
|
|
|
VM_FRAME_MAGIC_TOP = 0x44440001
|
|
|
|
VM_FRAME_MAGIC_CFUNC = 0x55550001
|
|
|
|
VM_FRAME_MAGIC_IFUNC = 0x66660001
|
|
|
|
VM_FRAME_MAGIC_EVAL = 0x77770001
|
|
|
|
VM_FRAME_MAGIC_RESCUE = 0x78880001
|
|
|
|
VM_FRAME_MAGIC_DUMMY = 0x79990001
|
|
|
|
|
|
|
|
VM_FRAME_MAGIC_MASK = 0x7fff0001
|
|
|
|
|
|
|
|
VM_FRAME_MAGIC_NAME = {
|
|
|
|
VM_FRAME_MAGIC_TOP: "TOP",
|
|
|
|
VM_FRAME_MAGIC_METHOD: "METHOD",
|
|
|
|
VM_FRAME_MAGIC_CLASS: "CLASS",
|
|
|
|
VM_FRAME_MAGIC_BLOCK: "BLOCK",
|
|
|
|
VM_FRAME_MAGIC_CFUNC: "CFUNC",
|
|
|
|
VM_FRAME_MAGIC_IFUNC: "IFUNC",
|
|
|
|
VM_FRAME_MAGIC_EVAL: "EVAL",
|
|
|
|
VM_FRAME_MAGIC_RESCUE: "RESCUE",
|
|
|
|
0: "-----"
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, debugger, command, result, internal_dict):
|
|
|
|
self.debugger = debugger
|
|
|
|
self.command = command
|
|
|
|
self.result = result
|
|
|
|
|
|
|
|
self.target = debugger.GetSelectedTarget()
|
|
|
|
self.process = self.target.GetProcess()
|
|
|
|
self.thread = self.process.GetSelectedThread()
|
|
|
|
self.frame = self.thread.GetSelectedFrame()
|
|
|
|
self.tRString = self.target.FindFirstType("struct RString").GetPointerType()
|
|
|
|
self.tRArray = self.target.FindFirstType("struct RArray").GetPointerType()
|
|
|
|
|
|
|
|
rb_cft_len = len("rb_control_frame_t")
|
|
|
|
method_type_length = sorted(map(len, self.VM_FRAME_MAGIC_NAME.values()), reverse=True)[0]
|
|
|
|
# cfp address, method type, function name
|
|
|
|
self.fmt = "%%-%ds %%-%ds %%s" % (rb_cft_len, method_type_length)
|
|
|
|
|
|
|
|
def vm_frame_magic(self, cfp):
|
|
|
|
ep = cfp.GetValueForExpressionPath("->ep")
|
|
|
|
frame_type = ep.GetChildAtIndex(0).GetValueAsUnsigned() & self.VM_FRAME_MAGIC_MASK
|
|
|
|
return self.VM_FRAME_MAGIC_NAME.get(frame_type, "(none)")
|
|
|
|
|
|
|
|
def rb_iseq_path_str(self, iseq):
|
2025-04-10 15:26:06 +02:00
|
|
|
tRBasic = self.target.FindFirstType("::RBasic").GetPointerType()
|
2020-10-14 16:41:07 -07:00
|
|
|
|
|
|
|
pathobj = iseq.GetValueForExpressionPath("->body->location.pathobj")
|
|
|
|
pathobj = pathobj.Cast(tRBasic)
|
|
|
|
flags = pathobj.GetValueForExpressionPath("->flags").GetValueAsUnsigned()
|
|
|
|
flType = flags & RUBY_T_MASK
|
|
|
|
|
|
|
|
if flType == RUBY_T_ARRAY:
|
|
|
|
pathobj = pathobj.Cast(self.tRArray)
|
|
|
|
|
|
|
|
if flags & RUBY_FL_USER1:
|
2022-06-17 11:40:11 +01:00
|
|
|
len = ((flags & (RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5|RUBY_FL_USER6|RUBY_FL_USER7|RUBY_FL_USER8|RUBY_FL_USER9)) >> (RUBY_FL_USHIFT+3))
|
2020-10-14 16:41:07 -07:00
|
|
|
ptr = pathobj.GetValueForExpressionPath("->as.ary")
|
|
|
|
else:
|
|
|
|
len = pathobj.GetValueForExpressionPath("->as.heap.len").GetValueAsSigned()
|
|
|
|
ptr = pathobj.GetValueForExpressionPath("->as.heap.ptr")
|
|
|
|
|
|
|
|
pathobj = ptr.GetChildAtIndex(0)
|
|
|
|
|
|
|
|
pathobj = pathobj.Cast(self.tRString)
|
|
|
|
ptr, len = string2cstr(pathobj)
|
|
|
|
err = lldb.SBError()
|
|
|
|
path = self.target.process.ReadMemory(ptr, len, err)
|
|
|
|
if err.Success():
|
|
|
|
return path.decode("utf-8")
|
|
|
|
else:
|
|
|
|
return "unknown"
|
|
|
|
|
|
|
|
def dump_iseq_frame(self, cfp, iseq):
|
|
|
|
m = self.vm_frame_magic(cfp)
|
|
|
|
|
|
|
|
if iseq.GetValueAsUnsigned():
|
|
|
|
iseq_label = iseq.GetValueForExpressionPath("->body->location.label")
|
|
|
|
path = self.rb_iseq_path_str(iseq)
|
|
|
|
ptr, len = string2cstr(iseq_label.Cast(self.tRString))
|
|
|
|
|
|
|
|
err = lldb.SBError()
|
|
|
|
iseq_name = self.target.process.ReadMemory(ptr, len, err)
|
|
|
|
if err.Success():
|
|
|
|
iseq_name = iseq_name.decode("utf-8")
|
|
|
|
else:
|
|
|
|
iseq_name = "error!!"
|
|
|
|
|
|
|
|
else:
|
|
|
|
print("No iseq", file=self.result)
|
|
|
|
|
|
|
|
print(self.fmt % (("%0#12x" % cfp.GetAddress().GetLoadAddress(self.target)), m, "%s %s" % (path, iseq_name)), file=self.result)
|
|
|
|
|
|
|
|
def dump_cfunc_frame(self, cfp):
|
|
|
|
print(self.fmt % ("%0#12x" % (cfp.GetAddress().GetLoadAddress(self.target)), "CFUNC", ""), file=self.result)
|
|
|
|
|
|
|
|
def print_bt(self, ec):
|
|
|
|
tRbExecutionContext_t = self.target.FindFirstType("rb_execution_context_t")
|
|
|
|
ec = ec.Cast(tRbExecutionContext_t.GetPointerType())
|
|
|
|
vm_stack = ec.GetValueForExpressionPath("->vm_stack")
|
|
|
|
vm_stack_size = ec.GetValueForExpressionPath("->vm_stack_size")
|
|
|
|
|
|
|
|
last_cfp_frame = ec.GetValueForExpressionPath("->cfp")
|
|
|
|
cfp_type_p = last_cfp_frame.GetType()
|
|
|
|
|
|
|
|
stack_top = vm_stack.GetValueAsUnsigned() + (
|
|
|
|
vm_stack_size.GetValueAsUnsigned() * vm_stack.GetType().GetByteSize())
|
|
|
|
|
|
|
|
cfp_frame_size = cfp_type_p.GetPointeeType().GetByteSize()
|
|
|
|
|
|
|
|
start_cfp = stack_top
|
|
|
|
# Skip dummy frames
|
|
|
|
start_cfp -= cfp_frame_size
|
|
|
|
start_cfp -= cfp_frame_size
|
|
|
|
|
|
|
|
last_cfp = last_cfp_frame.GetValueAsUnsigned()
|
|
|
|
|
|
|
|
size = ((start_cfp - last_cfp) / cfp_frame_size) + 1
|
|
|
|
|
|
|
|
print(self.fmt % ("rb_control_frame_t", "TYPE", ""), file=self.result)
|
|
|
|
|
|
|
|
curr_addr = start_cfp
|
|
|
|
|
|
|
|
while curr_addr >= last_cfp:
|
|
|
|
cfp = self.target.CreateValueFromAddress("cfp", lldb.SBAddress(curr_addr, self.target), cfp_type_p.GetPointeeType())
|
|
|
|
ep = cfp.GetValueForExpressionPath("->ep")
|
|
|
|
iseq = cfp.GetValueForExpressionPath("->iseq")
|
|
|
|
|
|
|
|
frame_type = ep.GetChildAtIndex(0).GetValueAsUnsigned() & self.VM_FRAME_MAGIC_MASK
|
|
|
|
|
|
|
|
if iseq.GetValueAsUnsigned():
|
|
|
|
pc = cfp.GetValueForExpressionPath("->pc")
|
|
|
|
if pc.GetValueAsUnsigned():
|
|
|
|
self.dump_iseq_frame(cfp, iseq)
|
|
|
|
else:
|
|
|
|
if frame_type == self.VM_FRAME_MAGIC_CFUNC:
|
|
|
|
self.dump_cfunc_frame(cfp)
|
|
|
|
|
|
|
|
curr_addr -= cfp_frame_size
|
|
|
|
|
2017-05-28 14:17:25 +00:00
|
|
|
def lldb_init(debugger):
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
global SIZEOF_VALUE
|
|
|
|
SIZEOF_VALUE = target.FindFirstType("VALUE").GetByteSize()
|
2017-08-02 05:27:25 +00:00
|
|
|
|
|
|
|
value_types = []
|
2017-05-28 14:17:25 +00:00
|
|
|
g = globals()
|
2022-06-15 09:25:07 -04:00
|
|
|
|
|
|
|
imemo_types = target.FindFirstType('enum imemo_type')
|
2022-07-06 13:20:53 -07:00
|
|
|
enum_members = imemo_types.GetEnumMembers()
|
2022-06-15 09:25:07 -04:00
|
|
|
|
2022-07-06 13:20:53 -07:00
|
|
|
for i in range(enum_members.GetSize()):
|
|
|
|
member = enum_members.GetTypeEnumMemberAtIndex(i)
|
2022-06-15 09:25:07 -04:00
|
|
|
g[member.GetName()] = member.GetValueAsUnsigned()
|
|
|
|
|
2017-05-28 14:17:25 +00:00
|
|
|
for enum in target.FindFirstGlobalVariable('ruby_dummy_gdb_enums'):
|
|
|
|
enum = enum.GetType()
|
|
|
|
members = enum.GetEnumMembers()
|
2019-09-24 21:05:29 +09:00
|
|
|
for i in range(0, members.GetSize()):
|
2017-05-28 14:17:25 +00:00
|
|
|
member = members.GetTypeEnumMemberAtIndex(i)
|
|
|
|
name = member.GetName()
|
|
|
|
value = member.GetValueAsUnsigned()
|
|
|
|
g[name] = value
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2017-08-02 05:27:25 +00:00
|
|
|
if name.startswith('RUBY_T_'):
|
|
|
|
value_types.append(name)
|
|
|
|
g['value_types'] = value_types
|
|
|
|
|
|
|
|
def string2cstr(rstring):
|
|
|
|
"""Returns the pointer to the C-string in the given String object"""
|
2019-10-09 09:01:11 +09:00
|
|
|
if rstring.TypeIsPointerType():
|
|
|
|
rstring = rstring.Dereference()
|
2017-08-02 05:27:25 +00:00
|
|
|
flags = rstring.GetValueForExpressionPath(".basic->flags").unsigned
|
|
|
|
if flags & RUBY_T_MASK != RUBY_T_STRING:
|
|
|
|
raise TypeError("not a string")
|
2023-08-29 19:31:53 -04:00
|
|
|
clen = int(rstring.GetValueForExpressionPath(".len").value, 0)
|
2017-08-02 05:27:25 +00:00
|
|
|
if flags & RUBY_FL_USER1:
|
|
|
|
cptr = int(rstring.GetValueForExpressionPath(".as.heap.ptr").value, 0)
|
|
|
|
else:
|
2021-10-28 08:58:59 +09:00
|
|
|
cptr = int(rstring.GetValueForExpressionPath(".as.embed.ary").location, 0)
|
2017-08-02 05:27:25 +00:00
|
|
|
return cptr, clen
|
|
|
|
|
2019-11-25 16:52:50 +09:00
|
|
|
def output_string(debugger, result, rstring):
|
2017-08-02 05:27:25 +00:00
|
|
|
cptr, clen = string2cstr(rstring)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(const char (*)[%d])%0#x" % (clen, cptr), result)
|
2017-08-02 05:27:25 +00:00
|
|
|
|
2017-05-28 14:17:25 +00:00
|
|
|
def fixnum_p(x):
|
|
|
|
return x & RUBY_FIXNUM_FLAG != 0
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2017-05-28 14:17:25 +00:00
|
|
|
def flonum_p(x):
|
|
|
|
return (x&RUBY_FLONUM_MASK) == RUBY_FLONUM_FLAG
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2018-10-04 14:00:39 +00:00
|
|
|
def static_sym_p(x):
|
|
|
|
return (x&~(~0<<RUBY_SPECIAL_SHIFT)) == RUBY_SYMBOL_FLAG
|
|
|
|
|
2018-10-03 04:07:16 +00:00
|
|
|
def append_command_output(debugger, command, result):
|
|
|
|
output1 = result.GetOutput()
|
|
|
|
debugger.GetCommandInterpreter().HandleCommand(command, result)
|
|
|
|
output2 = result.GetOutput()
|
|
|
|
result.Clear()
|
|
|
|
result.write(output1)
|
|
|
|
result.write(output2)
|
|
|
|
|
2023-10-25 15:08:26 +09:00
|
|
|
def append_expression(debugger, expression, result):
|
|
|
|
append_command_output(debugger, "expression " + expression, result)
|
|
|
|
|
2017-05-27 17:04:31 +00:00
|
|
|
def lldb_rp(debugger, command, result, internal_dict):
|
2018-10-02 19:14:24 +00:00
|
|
|
if not ('RUBY_Qfalse' in globals()):
|
|
|
|
lldb_init(debugger)
|
|
|
|
|
2017-05-27 17:04:31 +00:00
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
process = target.GetProcess()
|
|
|
|
thread = process.GetSelectedThread()
|
|
|
|
frame = thread.GetSelectedFrame()
|
2017-05-31 02:42:10 +00:00
|
|
|
if frame.IsValid():
|
|
|
|
val = frame.EvaluateExpression(command)
|
|
|
|
else:
|
|
|
|
val = target.EvaluateExpression(command)
|
|
|
|
error = val.GetError()
|
|
|
|
if error.Fail():
|
2019-09-24 21:05:29 +09:00
|
|
|
print(error, file=result)
|
2017-05-31 02:42:10 +00:00
|
|
|
return
|
2018-10-20 12:48:07 +00:00
|
|
|
lldb_inspect(debugger, target, result, val)
|
|
|
|
|
|
|
|
def lldb_inspect(debugger, target, result, val):
|
2017-05-27 17:04:31 +00:00
|
|
|
num = val.GetValueAsSigned()
|
2017-05-28 14:17:25 +00:00
|
|
|
if num == RUBY_Qfalse:
|
2019-09-24 21:05:29 +09:00
|
|
|
print('false', file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif num == RUBY_Qtrue:
|
2019-09-24 21:05:29 +09:00
|
|
|
print('true', file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif num == RUBY_Qnil:
|
2019-09-24 21:05:29 +09:00
|
|
|
print('nil', file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif num == RUBY_Qundef:
|
2019-09-24 21:05:29 +09:00
|
|
|
print('undef', file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif fixnum_p(num):
|
2019-09-24 21:05:29 +09:00
|
|
|
print(num >> 1, file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif flonum_p(num):
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "rb_float_value(%0#x)" % val.GetValueAsUnsigned(), result)
|
2018-10-04 14:00:39 +00:00
|
|
|
elif static_sym_p(num):
|
|
|
|
if num < 128:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("T_SYMBOL: %c" % num, file=result)
|
2018-10-04 14:00:39 +00:00
|
|
|
else:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("T_SYMBOL: (%x)" % num, file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "rb_id2name(%0#x)" % (num >> 8), result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif num & RUBY_IMMEDIATE_MASK:
|
2019-09-24 21:05:29 +09:00
|
|
|
print('immediate(%x)' % num, file=result)
|
2017-05-27 17:04:31 +00:00
|
|
|
else:
|
2025-04-10 15:26:06 +02:00
|
|
|
tRBasic = target.FindFirstType("::RBasic").GetPointerType()
|
2020-09-28 16:44:58 -07:00
|
|
|
|
2017-05-27 17:04:31 +00:00
|
|
|
val = val.Cast(tRBasic)
|
|
|
|
flags = val.GetValueForExpressionPath("->flags").GetValueAsUnsigned()
|
2019-10-24 23:37:19 +09:00
|
|
|
flaginfo = ""
|
2020-09-28 16:44:58 -07:00
|
|
|
|
|
|
|
page = get_page(lldb, target, val)
|
|
|
|
page_type = target.FindFirstType("struct heap_page").GetPointerType()
|
|
|
|
page.Cast(page_type)
|
|
|
|
|
2021-03-16 10:46:07 +00:00
|
|
|
dump_bits(target, result, page, val.GetValueAsUnsigned())
|
2020-09-28 16:44:58 -07:00
|
|
|
|
2017-05-28 14:17:25 +00:00
|
|
|
if (flags & RUBY_FL_PROMOTED) == RUBY_FL_PROMOTED:
|
2019-10-24 23:37:19 +09:00
|
|
|
flaginfo += "[PROMOTED] "
|
2019-05-09 12:27:44 -07:00
|
|
|
if (flags & RUBY_FL_FREEZE) == RUBY_FL_FREEZE:
|
2019-10-24 23:37:19 +09:00
|
|
|
flaginfo += "[FROZEN] "
|
2017-05-27 17:04:31 +00:00
|
|
|
flType = flags & RUBY_T_MASK
|
2017-05-28 14:17:25 +00:00
|
|
|
if flType == RUBY_T_NONE:
|
2019-10-24 23:37:19 +09:00
|
|
|
print('T_NONE: %s%s' % (flaginfo, val.Dereference()), file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif flType == RUBY_T_NIL:
|
2019-10-24 23:37:19 +09:00
|
|
|
print('T_NIL: %s%s' % (flaginfo, val.Dereference()), file=result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif flType == RUBY_T_OBJECT:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write('T_OBJECT: %s' % flaginfo)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RObject*)%0#x" % val.GetValueAsUnsigned(), result)
|
2018-10-03 09:54:25 +00:00
|
|
|
elif flType == RUBY_T_CLASS or flType == RUBY_T_MODULE or flType == RUBY_T_ICLASS:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write('T_%s: %s' % ('CLASS' if flType == RUBY_T_CLASS else 'MODULE' if flType == RUBY_T_MODULE else 'ICLASS', flaginfo))
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RClass*)%0#x" % val.GetValueAsUnsigned(), result)
|
2022-01-17 19:43:52 +09:00
|
|
|
tRClass = target.FindFirstType("struct RClass")
|
|
|
|
if not val.Cast(tRClass).GetChildMemberWithName("ptr").IsValid():
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct rb_classext_struct*)%0#x" % (val.GetValueAsUnsigned() + tRClass.GetByteSize()), result)
|
2017-05-28 14:17:25 +00:00
|
|
|
elif flType == RUBY_T_STRING:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write('T_STRING: %s' % flaginfo)
|
2021-09-28 19:55:07 +09:00
|
|
|
encidx = ((flags & RUBY_ENCODING_MASK)>>RUBY_ENCODING_SHIFT)
|
2021-09-29 22:31:24 +09:00
|
|
|
encname = target.FindFirstType("enum ruby_preserved_encindex").GetEnumMembers().GetTypeEnumMemberAtIndex(encidx).GetName()
|
|
|
|
if encname is not None:
|
|
|
|
result.write('[%s] ' % encname[14:])
|
|
|
|
else:
|
|
|
|
result.write('[enc=%d] ' % encidx)
|
2017-05-27 17:04:31 +00:00
|
|
|
tRString = target.FindFirstType("struct RString").GetPointerType()
|
2019-10-09 09:04:37 +09:00
|
|
|
ptr, len = string2cstr(val.Cast(tRString))
|
2020-04-26 12:28:56 +09:00
|
|
|
if len == 0:
|
|
|
|
result.write("(empty)\n")
|
|
|
|
else:
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(const char (*)[%d])%0#x" % (len, ptr), result)
|
2018-10-04 14:00:39 +00:00
|
|
|
elif flType == RUBY_T_SYMBOL:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write('T_SYMBOL: %s' % flaginfo)
|
2019-11-25 16:52:50 +09:00
|
|
|
tRSymbol = target.FindFirstType("struct RSymbol").GetPointerType()
|
|
|
|
val = val.Cast(tRSymbol)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, '(ID)%0#x ' % val.GetValueForExpressionPath("->id").GetValueAsUnsigned(), result)
|
2019-11-25 16:52:50 +09:00
|
|
|
tRString = target.FindFirstType("struct RString").GetPointerType()
|
|
|
|
output_string(debugger, result, val.GetValueForExpressionPath("->fstr").Cast(tRString))
|
2017-05-27 17:04:31 +00:00
|
|
|
elif flType == RUBY_T_ARRAY:
|
|
|
|
tRArray = target.FindFirstType("struct RArray").GetPointerType()
|
|
|
|
val = val.Cast(tRArray)
|
|
|
|
if flags & RUBY_FL_USER1:
|
2022-06-17 11:40:11 +01:00
|
|
|
len = ((flags & (RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5|RUBY_FL_USER6|RUBY_FL_USER7|RUBY_FL_USER8|RUBY_FL_USER9)) >> (RUBY_FL_USHIFT+3))
|
2018-10-03 04:07:16 +00:00
|
|
|
ptr = val.GetValueForExpressionPath("->as.ary")
|
2017-05-27 17:04:31 +00:00
|
|
|
else:
|
|
|
|
len = val.GetValueForExpressionPath("->as.heap.len").GetValueAsSigned()
|
2018-10-03 04:07:16 +00:00
|
|
|
ptr = val.GetValueForExpressionPath("->as.heap.ptr")
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write("T_ARRAY: %slen=%d" % (flaginfo, len))
|
2018-10-03 04:07:16 +00:00
|
|
|
if flags & RUBY_FL_USER1:
|
|
|
|
result.write(" (embed)")
|
|
|
|
elif flags & RUBY_FL_USER2:
|
|
|
|
shared = val.GetValueForExpressionPath("->as.heap.aux.shared").GetValueAsUnsigned()
|
2019-10-02 10:26:39 -10:00
|
|
|
result.write(" (shared) shared=%016x" % shared)
|
2018-10-03 04:07:16 +00:00
|
|
|
else:
|
|
|
|
capa = val.GetValueForExpressionPath("->as.heap.aux.capa").GetValueAsSigned()
|
|
|
|
result.write(" (ownership) capa=%d" % capa)
|
|
|
|
if len == 0:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write(" {(empty)}\n")
|
2018-10-03 04:07:16 +00:00
|
|
|
else:
|
|
|
|
result.write("\n")
|
2019-11-25 09:19:42 +09:00
|
|
|
if ptr.GetValueAsSigned() == 0:
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "-fx -- ((struct RArray*)%0#x)->as.ary" % val.GetValueAsUnsigned(), result)
|
2019-11-25 09:19:42 +09:00
|
|
|
else:
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "-Z %d -fx -- (const VALUE*)%0#x" % (len, ptr.GetValueAsUnsigned()), result)
|
2018-10-03 04:27:19 +00:00
|
|
|
elif flType == RUBY_T_HASH:
|
2019-10-24 23:37:19 +09:00
|
|
|
result.write("T_HASH: %s" % flaginfo)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RHash *) %0#x" % val.GetValueAsUnsigned(), result)
|
2018-10-03 04:27:19 +00:00
|
|
|
elif flType == RUBY_T_BIGNUM:
|
|
|
|
tRBignum = target.FindFirstType("struct RBignum").GetPointerType()
|
|
|
|
val = val.Cast(tRBignum)
|
2020-06-23 15:52:37 +09:00
|
|
|
sign = '+' if (flags & RUBY_FL_USER1) != 0 else '-'
|
2018-10-03 04:27:19 +00:00
|
|
|
if flags & RUBY_FL_USER2:
|
|
|
|
len = ((flags & (RUBY_FL_USER3|RUBY_FL_USER4|RUBY_FL_USER5)) >> (RUBY_FL_USHIFT+3))
|
2020-06-23 15:52:37 +09:00
|
|
|
print("T_BIGNUM: sign=%s len=%d (embed)" % (sign, len), file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "((struct RBignum *) %0#x)->as.ary" % val.GetValueAsUnsigned(), result)
|
2018-10-03 04:27:19 +00:00
|
|
|
else:
|
|
|
|
len = val.GetValueForExpressionPath("->as.heap.len").GetValueAsSigned()
|
2020-06-23 15:52:37 +09:00
|
|
|
print("T_BIGNUM: sign=%s len=%d" % (sign, len), file=result)
|
2019-09-24 21:05:29 +09:00
|
|
|
print(val.Dereference(), file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "-Z %x -fx -- (const BDIGIT*)((struct RBignum*)%d)->as.heap.digits" % (len, val.GetValueAsUnsigned()), result)
|
|
|
|
# append_expression(debugger, "((struct RBignum *) %0#x)->as.heap.digits / %d" % (val.GetValueAsUnsigned(), len), result)
|
2018-10-03 04:27:19 +00:00
|
|
|
elif flType == RUBY_T_FLOAT:
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "((struct RFloat *)%d)->float_value" % val.GetValueAsUnsigned(), result)
|
2018-10-20 12:48:07 +00:00
|
|
|
elif flType == RUBY_T_RATIONAL:
|
|
|
|
tRRational = target.FindFirstType("struct RRational").GetPointerType()
|
|
|
|
val = val.Cast(tRRational)
|
|
|
|
lldb_inspect(debugger, target, result, val.GetValueForExpressionPath("->num"))
|
|
|
|
output = result.GetOutput()
|
|
|
|
result.Clear()
|
|
|
|
result.write("(Rational) " + output.rstrip() + " / ")
|
|
|
|
lldb_inspect(debugger, target, result, val.GetValueForExpressionPath("->den"))
|
2018-10-20 17:19:37 +00:00
|
|
|
elif flType == RUBY_T_COMPLEX:
|
|
|
|
tRComplex = target.FindFirstType("struct RComplex").GetPointerType()
|
|
|
|
val = val.Cast(tRComplex)
|
|
|
|
lldb_inspect(debugger, target, result, val.GetValueForExpressionPath("->real"))
|
|
|
|
real = result.GetOutput().rstrip()
|
|
|
|
result.Clear()
|
|
|
|
lldb_inspect(debugger, target, result, val.GetValueForExpressionPath("->imag"))
|
|
|
|
imag = result.GetOutput().rstrip()
|
|
|
|
result.Clear()
|
|
|
|
if not imag.startswith("-"):
|
|
|
|
imag = "+" + imag
|
2019-09-24 21:05:29 +09:00
|
|
|
print("(Complex) " + real + imag + "i", file=result)
|
2019-04-02 21:13:22 +00:00
|
|
|
elif flType == RUBY_T_REGEXP:
|
2019-04-01 22:53:34 +00:00
|
|
|
tRRegex = target.FindFirstType("struct RRegexp").GetPointerType()
|
|
|
|
val = val.Cast(tRRegex)
|
2019-09-25 16:58:24 +09:00
|
|
|
print("(Regex) ->src {", file=result)
|
2019-04-01 22:53:34 +00:00
|
|
|
lldb_inspect(debugger, target, result, val.GetValueForExpressionPath("->src"))
|
2019-09-24 21:05:29 +09:00
|
|
|
print("}", file=result)
|
2018-10-01 14:48:54 +00:00
|
|
|
elif flType == RUBY_T_DATA:
|
|
|
|
tRTypedData = target.FindFirstType("struct RTypedData").GetPointerType()
|
|
|
|
val = val.Cast(tRTypedData)
|
|
|
|
flag = val.GetValueForExpressionPath("->typed_flag")
|
|
|
|
if flag.GetValueAsUnsigned() == 1:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("T_DATA: %s" % val.GetValueForExpressionPath("->type->wrap_struct_name"), file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RTypedData *) %0#x" % val.GetValueAsUnsigned(), result)
|
2018-10-01 14:48:54 +00:00
|
|
|
else:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("T_DATA:", file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RData *) %0#x" % val.GetValueAsUnsigned(), result)
|
2019-08-29 14:51:34 -07:00
|
|
|
elif flType == RUBY_T_NODE:
|
|
|
|
tRTypedData = target.FindFirstType("struct RNode").GetPointerType()
|
|
|
|
nd_type = (flags & RUBY_NODE_TYPEMASK) >> RUBY_NODE_TYPESHIFT
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "(node_type) %d" % nd_type, result)
|
2019-08-29 14:51:34 -07:00
|
|
|
val = val.Cast(tRTypedData)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RNode *) %0#x" % val.GetValueAsUnsigned(), result)
|
2020-05-07 14:19:08 -07:00
|
|
|
elif flType == RUBY_T_MOVED:
|
|
|
|
tRTypedData = target.FindFirstType("struct RMoved").GetPointerType()
|
|
|
|
val = val.Cast(tRTypedData)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RMoved *) %0#x" % val.GetValueAsUnsigned(), result)
|
2020-09-02 16:42:14 -07:00
|
|
|
elif flType == RUBY_T_MATCH:
|
|
|
|
tRTypedData = target.FindFirstType("struct RMatch").GetPointerType()
|
|
|
|
val = val.Cast(tRTypedData)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RMatch *) %0#x" % val.GetValueAsUnsigned(), result)
|
2020-05-07 15:52:29 -07:00
|
|
|
elif flType == RUBY_T_IMEMO:
|
|
|
|
# I'm not sure how to get IMEMO_MASK out of lldb. It's not in globals()
|
|
|
|
imemo_type = (flags >> RUBY_FL_USHIFT) & 0x0F # IMEMO_MASK
|
2022-10-03 11:14:32 -04:00
|
|
|
|
2020-05-07 15:52:29 -07:00
|
|
|
print("T_IMEMO: ", file=result)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "(enum imemo_type) %d" % imemo_type, result)
|
|
|
|
append_expression(debugger, "*(struct MEMO *) %0#x" % val.GetValueAsUnsigned(), result)
|
2022-06-21 16:12:27 -04:00
|
|
|
elif flType == RUBY_T_STRUCT:
|
|
|
|
tRTypedData = target.FindFirstType("struct RStruct").GetPointerType()
|
|
|
|
val = val.Cast(tRTypedData)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RStruct *) %0#x" % val.GetValueAsUnsigned(), result)
|
2020-08-27 09:00:14 -07:00
|
|
|
elif flType == RUBY_T_ZOMBIE:
|
|
|
|
tRZombie = target.FindFirstType("struct RZombie").GetPointerType()
|
|
|
|
val = val.Cast(tRZombie)
|
2023-10-25 15:08:26 +09:00
|
|
|
append_expression(debugger, "*(struct RZombie *) %0#x" % val.GetValueAsUnsigned(), result)
|
2018-10-03 04:27:19 +00:00
|
|
|
else:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("Not-handled type %0#x" % flType, file=result)
|
|
|
|
print(val, file=result)
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2017-08-02 05:27:25 +00:00
|
|
|
def count_objects(debugger, command, ctx, result, internal_dict):
|
|
|
|
objspace = ctx.frame.EvaluateExpression("ruby_current_vm->objspace")
|
|
|
|
num_pages = objspace.GetValueForExpressionPath(".heap_pages.allocated_pages").unsigned
|
|
|
|
|
|
|
|
counts = {}
|
|
|
|
total = 0
|
|
|
|
for t in range(0x00, RUBY_T_MASK+1):
|
|
|
|
counts[t] = 0
|
|
|
|
|
|
|
|
for i in range(0, num_pages):
|
2019-09-24 21:05:29 +09:00
|
|
|
print("\rcounting... %d/%d" % (i, num_pages), end="")
|
2017-08-02 05:27:25 +00:00
|
|
|
page = objspace.GetValueForExpressionPath('.heap_pages.sorted[%d]' % i)
|
|
|
|
p = page.GetChildMemberWithName('start')
|
|
|
|
num_slots = page.GetChildMemberWithName('total_slots').unsigned
|
|
|
|
for j in range(0, num_slots):
|
|
|
|
obj = p.GetValueForExpressionPath('[%d]' % j)
|
|
|
|
flags = obj.GetValueForExpressionPath('.as.basic.flags').unsigned
|
|
|
|
obj_type = flags & RUBY_T_MASK
|
|
|
|
counts[obj_type] += 1
|
|
|
|
total += num_slots
|
|
|
|
|
2019-09-25 16:58:24 +09:00
|
|
|
print("\rTOTAL: %d, FREE: %d" % (total, counts[0x00]))
|
2017-08-02 05:27:25 +00:00
|
|
|
for sym in value_types:
|
2019-09-24 21:05:29 +09:00
|
|
|
print("%s: %d" % (sym, counts[globals()[sym]]))
|
2017-08-02 05:27:25 +00:00
|
|
|
|
|
|
|
def stack_dump_raw(debugger, command, ctx, result, internal_dict):
|
|
|
|
ctx.frame.EvaluateExpression("rb_vmdebug_stack_dump_raw_current()")
|
|
|
|
|
2020-09-28 16:44:58 -07:00
|
|
|
def check_bits(page, bitmap_name, bitmap_index, bitmap_bit, v):
|
|
|
|
bits = page.GetChildMemberWithName(bitmap_name)
|
|
|
|
plane = bits.GetChildAtIndex(bitmap_index).GetValueAsUnsigned()
|
|
|
|
if (plane & bitmap_bit) != 0:
|
|
|
|
return v
|
|
|
|
else:
|
|
|
|
return ' '
|
|
|
|
|
2020-09-02 16:42:56 -07:00
|
|
|
def heap_page_body(debugger, command, ctx, result, internal_dict):
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
process = target.GetProcess()
|
|
|
|
thread = process.GetSelectedThread()
|
|
|
|
frame = thread.GetSelectedFrame()
|
|
|
|
|
|
|
|
val = frame.EvaluateExpression(command)
|
|
|
|
page = get_page_body(lldb, target, val)
|
|
|
|
print("Page body address: ", page.GetAddress(), file=result)
|
|
|
|
print(page, file=result)
|
|
|
|
|
|
|
|
def get_page_body(lldb, target, val):
|
|
|
|
tHeapPageBody = target.FindFirstType("struct heap_page_body")
|
|
|
|
addr = val.GetValueAsUnsigned()
|
|
|
|
page_addr = addr & ~(HEAP_PAGE_ALIGN_MASK)
|
|
|
|
address = lldb.SBAddress(page_addr, target)
|
|
|
|
return target.CreateValueFromAddress("page", address, tHeapPageBody)
|
|
|
|
|
|
|
|
def get_page(lldb, target, val):
|
|
|
|
body = get_page_body(lldb, target, val)
|
|
|
|
return body.GetValueForExpressionPath("->header.page")
|
|
|
|
|
2017-08-02 05:27:25 +00:00
|
|
|
def dump_node(debugger, command, ctx, result, internal_dict):
|
|
|
|
args = shlex.split(command)
|
|
|
|
if not args:
|
|
|
|
return
|
|
|
|
node = args[0]
|
|
|
|
|
|
|
|
dump = ctx.frame.EvaluateExpression("(struct RString*)rb_parser_dump_tree((NODE*)(%s), 0)" % node)
|
2019-11-25 16:52:50 +09:00
|
|
|
output_string(ctx, result, dump)
|
2017-05-27 17:04:31 +00:00
|
|
|
|
2020-10-14 16:41:07 -07:00
|
|
|
def rb_backtrace(debugger, command, result, internal_dict):
|
2022-06-15 21:41:15 +01:00
|
|
|
if not ('RUBY_Qfalse' in globals()):
|
|
|
|
lldb_init(debugger)
|
2020-10-14 16:41:07 -07:00
|
|
|
bt = BackTrace(debugger, command, result, internal_dict)
|
|
|
|
frame = bt.frame
|
|
|
|
|
|
|
|
if command:
|
|
|
|
if frame.IsValid():
|
|
|
|
val = frame.EvaluateExpression(command)
|
|
|
|
else:
|
|
|
|
val = target.EvaluateExpression(command)
|
|
|
|
|
|
|
|
error = val.GetError()
|
|
|
|
if error.Fail():
|
|
|
|
print >> result, error
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
print("Need an EC for now")
|
|
|
|
|
|
|
|
bt.print_bt(val)
|
|
|
|
|
2021-03-16 10:46:07 +00:00
|
|
|
def dump_bits(target, result, page, object_address, end = "\n"):
|
2025-04-08 12:52:49 -04:00
|
|
|
slot_size = page.GetChildMemberWithName("heap").GetChildMemberWithName("slot_size").unsigned
|
|
|
|
byte_size = 40 ** math.floor(math.log(slot_size, 40))
|
2021-03-16 10:46:07 +00:00
|
|
|
tUintPtr = target.FindFirstType("uintptr_t") # bits_t
|
|
|
|
|
2025-04-08 12:52:49 -04:00
|
|
|
num_in_page = (object_address & HEAP_PAGE_ALIGN_MASK) // byte_size;
|
2021-03-16 10:46:07 +00:00
|
|
|
bits_bitlength = tUintPtr.GetByteSize() * 8
|
|
|
|
bitmap_index = num_in_page // bits_bitlength
|
|
|
|
bitmap_offset = num_in_page & (bits_bitlength - 1)
|
|
|
|
bitmap_bit = 1 << bitmap_offset
|
|
|
|
|
|
|
|
print("bits: [%s%s%s%s%s]" % (
|
|
|
|
check_bits(page, "uncollectible_bits", bitmap_index, bitmap_bit, "L"),
|
|
|
|
check_bits(page, "mark_bits", bitmap_index, bitmap_bit, "M"),
|
|
|
|
check_bits(page, "pinned_bits", bitmap_index, bitmap_bit, "P"),
|
|
|
|
check_bits(page, "marking_bits", bitmap_index, bitmap_bit, "R"),
|
|
|
|
check_bits(page, "wb_unprotected_bits", bitmap_index, bitmap_bit, "U"),
|
|
|
|
), end=end, file=result)
|
2021-03-16 10:47:35 +00:00
|
|
|
|
2021-04-27 12:13:29 +01:00
|
|
|
class HeapPageIter:
|
|
|
|
def __init__(self, page, target):
|
|
|
|
self.page = page
|
|
|
|
self.target = target
|
|
|
|
self.start = page.GetChildMemberWithName('start').GetValueAsUnsigned();
|
|
|
|
self.num_slots = page.GetChildMemberWithName('total_slots').unsigned
|
Rename size_pool -> heap
Now that we've inlined the eden_heap into the size_pool, we should
rename the size_pool to heap. So that Ruby contains multiple heaps, with
different sized objects.
The term heap as a collection of memory pages is more in memory
management nomenclature, whereas size_pool was a name chosen out of
necessity during the development of the Variable Width Allocation
features of Ruby.
The concept of size pools was introduced in order to facilitate
different sized objects (other than the default 40 bytes). They wrapped
the eden heap and the tomb heap, and some related state, and provided a
reasonably simple way of duplicating all related concerns, to provide
multiple pools that all shared the same structure but held different
objects.
Since then various changes have happend in Ruby's memory layout:
* The concept of tomb heaps has been replaced by a global free pages list,
with each page having it's slot size reconfigured at the point when it
is resurrected
* the eden heap has been inlined into the size pool itself, so that now
the size pool directly controls the free_pages list, the sweeping
page, the compaction cursor and the other state that was previously
being managed by the eden heap.
Now that there is no need for a heap wrapper, we should refer to the
collection of pages containing Ruby objects as a heap again rather than
a size pool
2024-10-03 13:53:49 +01:00
|
|
|
self.slot_size = page.GetChildMemberWithName('heap').GetChildMemberWithName('slot_size').unsigned
|
2021-04-27 12:13:29 +01:00
|
|
|
self.counter = 0
|
2025-04-10 15:26:06 +02:00
|
|
|
self.tRBasic = target.FindFirstType("::RBasic")
|
2021-04-27 12:13:29 +01:00
|
|
|
|
2021-04-29 17:02:02 +01:00
|
|
|
def is_valid(self):
|
|
|
|
heap_page_header_size = self.target.FindFirstType("struct heap_page_header").GetByteSize()
|
2021-08-24 13:14:23 -04:00
|
|
|
rvalue_size = self.slot_size
|
|
|
|
heap_page_obj_limit = int((HEAP_PAGE_SIZE - heap_page_header_size) / self.slot_size)
|
2021-04-29 17:02:02 +01:00
|
|
|
|
2021-05-06 15:51:47 +00:00
|
|
|
return (heap_page_obj_limit - 1) <= self.num_slots <= heap_page_obj_limit
|
2021-04-29 17:02:02 +01:00
|
|
|
|
2021-04-27 12:13:29 +01:00
|
|
|
def __iter__(self):
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __next__(self):
|
|
|
|
if self.counter < self.num_slots:
|
2021-08-24 13:14:23 -04:00
|
|
|
obj_addr_i = self.start + (self.counter * self.slot_size)
|
2021-04-27 12:13:29 +01:00
|
|
|
obj_addr = lldb.SBAddress(obj_addr_i, self.target)
|
|
|
|
slot_info = (self.counter, obj_addr_i, self.target.CreateValueFromAddress("object", obj_addr, self.tRBasic))
|
|
|
|
self.counter += 1
|
|
|
|
|
|
|
|
return slot_info
|
|
|
|
else:
|
|
|
|
raise StopIteration
|
|
|
|
|
|
|
|
|
2021-04-27 12:21:24 +01:00
|
|
|
def dump_page_internal(page, target, process, thread, frame, result, debugger, highlight=None):
|
2021-03-16 10:47:35 +00:00
|
|
|
if not ('RUBY_Qfalse' in globals()):
|
|
|
|
lldb_init(debugger)
|
|
|
|
|
2021-04-27 12:13:29 +01:00
|
|
|
ruby_type_map = ruby_types(debugger)
|
2021-03-16 10:47:35 +00:00
|
|
|
|
2021-04-27 12:13:29 +01:00
|
|
|
freelist = []
|
|
|
|
fl_start = page.GetChildMemberWithName('freelist').GetValueAsUnsigned()
|
2025-04-08 12:52:49 -04:00
|
|
|
free_slot = target.FindFirstType("struct free_slot")
|
2021-03-16 10:47:35 +00:00
|
|
|
|
2021-04-27 12:13:29 +01:00
|
|
|
while fl_start > 0:
|
|
|
|
freelist.append(fl_start)
|
|
|
|
obj_addr = lldb.SBAddress(fl_start, target)
|
2025-04-08 12:52:49 -04:00
|
|
|
obj = target.CreateValueFromAddress("object", obj_addr, free_slot)
|
|
|
|
fl_start = obj.GetChildMemberWithName("next").GetValueAsUnsigned()
|
2021-04-27 12:13:29 +01:00
|
|
|
|
2021-04-29 17:02:02 +01:00
|
|
|
page_iter = HeapPageIter(page, target)
|
|
|
|
if page_iter.is_valid():
|
|
|
|
for (page_index, obj_addr, obj) in page_iter:
|
|
|
|
dump_bits(target, result, page, obj_addr, end= " ")
|
|
|
|
flags = obj.GetChildMemberWithName('flags').GetValueAsUnsigned()
|
|
|
|
flType = flags & RUBY_T_MASK
|
|
|
|
|
|
|
|
flidx = ' '
|
|
|
|
if flType == RUBY_T_NONE:
|
|
|
|
try:
|
|
|
|
flidx = "%3d" % freelist.index(obj_addr)
|
|
|
|
except ValueError:
|
2022-05-26 15:42:42 -04:00
|
|
|
flidx = ' -1'
|
2021-04-27 12:13:29 +01:00
|
|
|
|
2022-05-26 15:42:42 -04:00
|
|
|
if flType == RUBY_T_NONE:
|
|
|
|
klass = obj.GetChildMemberWithName('klass').GetValueAsUnsigned()
|
|
|
|
result_str = "%s idx: [%3d] freelist_idx: {%s} Addr: %0#x (flags: %0#x, next: %0#x)" % (rb_type(flags, ruby_type_map), page_index, flidx, obj_addr, flags, klass)
|
|
|
|
else:
|
|
|
|
result_str = "%s idx: [%3d] freelist_idx: {%s} Addr: %0#x (flags: %0#x)" % (rb_type(flags, ruby_type_map), page_index, flidx, obj_addr, flags)
|
2021-04-27 12:21:24 +01:00
|
|
|
|
2021-04-29 17:02:02 +01:00
|
|
|
if highlight == obj_addr:
|
|
|
|
result_str = ' '.join([result_str, "<<<<<"])
|
|
|
|
|
|
|
|
print(result_str, file=result)
|
|
|
|
else:
|
|
|
|
print("%s is not a valid heap page" % page, file=result)
|
2021-04-27 12:21:24 +01:00
|
|
|
|
2021-03-16 10:47:35 +00:00
|
|
|
|
|
|
|
|
2021-04-27 12:17:55 +01:00
|
|
|
def dump_page(debugger, command, result, internal_dict):
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
process = target.GetProcess()
|
|
|
|
thread = process.GetSelectedThread()
|
|
|
|
frame = thread.GetSelectedFrame()
|
|
|
|
|
|
|
|
tHeapPageP = target.FindFirstType("struct heap_page").GetPointerType()
|
|
|
|
page = frame.EvaluateExpression(command)
|
|
|
|
page = page.Cast(tHeapPageP)
|
|
|
|
|
|
|
|
dump_page_internal(page, target, process, thread, frame, result, debugger)
|
|
|
|
|
|
|
|
|
|
|
|
def dump_page_rvalue(debugger, command, result, internal_dict):
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
process = target.GetProcess()
|
|
|
|
thread = process.GetSelectedThread()
|
|
|
|
frame = thread.GetSelectedFrame()
|
|
|
|
|
|
|
|
val = frame.EvaluateExpression(command)
|
|
|
|
page = get_page(lldb, target, val)
|
|
|
|
page_type = target.FindFirstType("struct heap_page").GetPointerType()
|
|
|
|
page.Cast(page_type)
|
|
|
|
|
2021-04-27 12:21:24 +01:00
|
|
|
dump_page_internal(page, target, process, thread, frame, result, debugger, highlight=val.GetValueAsUnsigned())
|
2021-04-27 12:17:55 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2021-03-16 10:47:35 +00:00
|
|
|
def rb_type(flags, ruby_types):
|
|
|
|
flType = flags & RUBY_T_MASK
|
|
|
|
return "%-10s" % (ruby_types.get(flType, ("%0#x" % flType)))
|
|
|
|
|
|
|
|
def ruby_types(debugger):
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
|
|
|
|
types = {}
|
|
|
|
for enum in target.FindFirstGlobalVariable('ruby_dummy_gdb_enums'):
|
|
|
|
enum = enum.GetType()
|
|
|
|
members = enum.GetEnumMembers()
|
|
|
|
for i in range(0, members.GetSize()):
|
|
|
|
member = members.GetTypeEnumMemberAtIndex(i)
|
|
|
|
name = member.GetName()
|
|
|
|
value = member.GetValueAsUnsigned()
|
|
|
|
|
|
|
|
if name.startswith('RUBY_T_'):
|
|
|
|
types[value] = name.replace('RUBY_', '')
|
|
|
|
|
|
|
|
return types
|
|
|
|
|
2021-05-24 16:02:17 -07:00
|
|
|
def rb_ary_entry(target, ary, idx, result):
|
|
|
|
tRArray = target.FindFirstType("struct RArray").GetPointerType()
|
|
|
|
ary = ary.Cast(tRArray)
|
|
|
|
flags = ary.GetValueForExpressionPath("->flags").GetValueAsUnsigned()
|
|
|
|
|
|
|
|
if flags & RUBY_FL_USER1:
|
|
|
|
ptr = ary.GetValueForExpressionPath("->as.ary")
|
|
|
|
else:
|
|
|
|
ptr = ary.GetValueForExpressionPath("->as.heap.ptr")
|
|
|
|
|
|
|
|
ptr_addr = ptr.GetValueAsUnsigned() + (idx * ptr.GetType().GetByteSize())
|
|
|
|
return target.CreateValueFromAddress("ary_entry[%d]" % idx, lldb.SBAddress(ptr_addr, target), ptr.GetType().GetPointeeType())
|
|
|
|
|
|
|
|
def rb_id_to_serial(id_val):
|
|
|
|
if id_val > tLAST_OP_ID:
|
|
|
|
return id_val >> RUBY_ID_SCOPE_SHIFT
|
|
|
|
else:
|
|
|
|
return id_val
|
|
|
|
|
|
|
|
def rb_id2str(debugger, command, result, internal_dict):
|
|
|
|
if not ('RUBY_Qfalse' in globals()):
|
|
|
|
lldb_init(debugger)
|
|
|
|
|
|
|
|
target = debugger.GetSelectedTarget()
|
|
|
|
process = target.GetProcess()
|
|
|
|
thread = process.GetSelectedThread()
|
|
|
|
frame = thread.GetSelectedFrame()
|
|
|
|
global_symbols = target.FindFirstGlobalVariable("ruby_global_symbols")
|
|
|
|
|
|
|
|
id_val = frame.EvaluateExpression(command).GetValueAsUnsigned()
|
|
|
|
num = rb_id_to_serial(id_val)
|
|
|
|
|
|
|
|
last_id = global_symbols.GetChildMemberWithName("last_id").GetValueAsUnsigned()
|
|
|
|
ID_ENTRY_SIZE = 2
|
|
|
|
ID_ENTRY_UNIT = int(target.FindFirstGlobalVariable("ID_ENTRY_UNIT").GetValue())
|
|
|
|
|
|
|
|
ids = global_symbols.GetChildMemberWithName("ids")
|
|
|
|
|
|
|
|
if (num <= last_id):
|
|
|
|
idx = num // ID_ENTRY_UNIT
|
|
|
|
ary = rb_ary_entry(target, ids, idx, result)
|
|
|
|
pos = (num % ID_ENTRY_UNIT) * ID_ENTRY_SIZE
|
|
|
|
id_str = rb_ary_entry(target, ary, pos, result)
|
|
|
|
lldb_inspect(debugger, target, result, id_str)
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
# END FUNCTION STYLE DECLS
|
|
|
|
|
|
|
|
|
|
|
|
load_dir, _ = os.path.split(os.path.realpath(__file__))
|
|
|
|
|
2022-07-13 18:14:44 +01:00
|
|
|
for fname in glob.glob(f"{load_dir}/lldb_rb/commands/*_command.py"):
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
_, basename = os.path.split(fname)
|
|
|
|
mname, _ = os.path.splitext(basename)
|
2022-06-15 14:06:36 +01:00
|
|
|
|
2022-07-13 18:14:44 +01:00
|
|
|
exec(f"import lldb_rb.commands.{mname}")
|
2022-06-15 14:06:36 +01:00
|
|
|
|
2017-05-27 17:04:31 +00:00
|
|
|
def __lldb_init_module(debugger, internal_dict):
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
# Register all classes that subclass RbBaseCommand
|
|
|
|
|
|
|
|
for memname, mem in inspect.getmembers(sys.modules["lldb_rb.rb_base_command"]):
|
2023-03-16 09:54:36 +00:00
|
|
|
if memname == "RbBaseCommand":
|
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
`lldb_cruby.py` manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
`lldb_disasm.py` was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code to `lldb_cruby.py`
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
```
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
```
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the `commands` directory and the
filename must end in `_command.py`
- The package must implement a class whose name ends in `Command`
- The class inherits from `RbBaseCommand` or at minimum a class that
shares the same interface as `RbBaseCommand` (at minimum this means
defining `__init__` and `__call__`, and using `__call__` to call
`call` which is defined in the subclasses).
- The class must have a class variable `package` that is a String. This
is the name of the command you'll call in the `lldb` debugger.
2022-07-13 13:18:03 +01:00
|
|
|
for sclass in mem.__subclasses__():
|
|
|
|
sclass.register_lldb_command(debugger, f"{__name__}.{sclass.__module__}")
|
|
|
|
|
|
|
|
|
|
|
|
## FUNCTION INITS - These should be removed when converted to class commands
|
2023-03-16 09:54:36 +00:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.lldb_rp old_rp")
|
2017-08-02 05:27:25 +00:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.count_objects rb_count_objects")
|
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.stack_dump_raw SDR")
|
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.dump_node dump_node")
|
2020-09-02 16:42:56 -07:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.heap_page_body heap_page_body")
|
2020-10-14 16:41:07 -07:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.rb_backtrace rbbt")
|
2021-03-16 10:47:35 +00:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.dump_page dump_page")
|
2021-04-27 12:17:55 +01:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.dump_page_rvalue dump_page_rvalue")
|
2022-09-26 17:08:31 +01:00
|
|
|
debugger.HandleCommand("command script add -f lldb_cruby.rb_id2str old_rb_id2str")
|
2022-08-18 11:44:25 +01:00
|
|
|
|
|
|
|
lldb_rb.rb_base_command.RbBaseCommand.lldb_init(debugger)
|
|
|
|
|
2019-09-24 21:05:29 +09:00
|
|
|
print("lldb scripts for ruby has been installed.")
|