diff options
author | Matt Valentine-House <matt@eightbitraptor.com> | 2022-07-13 13:18:03 +0100 |
---|---|---|
committer | Peter Zhu <peter@peterzhu.ca> | 2022-08-18 13:25:32 -0400 |
commit | f1ccfa0c2c200c9443fbfc3f1ac3acbdd3e35559 (patch) | |
tree | 0ff12dcff017bd8a5ac4929fbe318ba766b57ce4 /misc | |
parent | d903e7672637d5a834847820a4e18b00ee30f380 (diff) |
[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.
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/6129
Diffstat (limited to 'misc')
-rw-r--r-- | misc/commands/command_template.py | 25 | ||||
-rw-r--r-- | misc/constants.py | 4 | ||||
-rwxr-xr-x | misc/lldb_cruby.py | 29 | ||||
-rw-r--r-- | misc/rb_base_command.py | 68 |
4 files changed, 121 insertions, 5 deletions
diff --git a/misc/commands/command_template.py b/misc/commands/command_template.py new file mode 100644 index 0000000000..bbc4b09157 --- /dev/null +++ b/misc/commands/command_template.py @@ -0,0 +1,25 @@ +# This is a command template for implementing a helper function inside LLDB. To +# use this file +# 1. Copy it and rename the copy so it ends with `_command.py`. +# 2. Rename the class to something descriptive that ends with Command. +# 3. Change the program variable to be a descriptive command name +# 4. Ensure you are inheriting from RbBaseCommand or another command that +# implements the same interfact + +# This test command inherits from RbBaseCommand which provides access to Ruby +# globals and utility helpers +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): + # This method will be called once the LLDB environment has been setup. + # You will have access to self.target, self.process, self.frame, and + # self.thread + # + # This is where we should implement our command logic + pass diff --git a/misc/constants.py b/misc/constants.py new file mode 100644 index 0000000000..ec3050a399 --- /dev/null +++ b/misc/constants.py @@ -0,0 +1,4 @@ +HEAP_PAGE_ALIGN_LOG = 16 +HEAP_PAGE_ALIGN_MASK = (~(~0 << HEAP_PAGE_ALIGN_LOG)) +HEAP_PAGE_ALIGN = (1 << HEAP_PAGE_ALIGN_LOG) +HEAP_PAGE_SIZE = HEAP_PAGE_ALIGN diff --git a/misc/lldb_cruby.py b/misc/lldb_cruby.py index c38b9c62a0..de8628754c 100755 --- a/misc/lldb_cruby.py +++ b/misc/lldb_cruby.py @@ -9,15 +9,16 @@ from __future__ import print_function import lldb import os +import inspect +import sys import shlex import platform +import glob -HEAP_PAGE_ALIGN_LOG = 16 - -HEAP_PAGE_ALIGN_MASK = (~(~0 << HEAP_PAGE_ALIGN_LOG)) -HEAP_PAGE_ALIGN = (1 << HEAP_PAGE_ALIGN_LOG) -HEAP_PAGE_SIZE = HEAP_PAGE_ALIGN +from constants import * +# BEGIN FUNCTION STYLE DECLS +# This will be refactored to use class style decls in the misc/commands dir class BackTrace: VM_FRAME_MAGIC_METHOD = 0x11110001 VM_FRAME_MAGIC_BLOCK = 0x22220001 @@ -740,9 +741,27 @@ def rb_rclass_ext(debugger, command, result, internal_dict): rclass_addr = target.EvaluateExpression(command).Cast(uintptr_t) rclass_ext_addr = (rclass_addr.GetValueAsUnsigned() + rclass_t.GetByteSize()) debugger.HandleCommand("p *(rb_classext_t *)%0#x" % rclass_ext_addr) +# END FUNCTION STYLE DECLS + + +load_dir, _ = os.path.split(os.path.realpath(__file__)) + +for fname in glob.glob(f"{load_dir}/commands/*_command.py"): + _, basename = os.path.split(fname) + mname, _ = os.path.splitext(basename) + exec(f"import commands.{mname}") def __lldb_init_module(debugger, internal_dict): + # Register all classes that subclass RbBaseCommand + + for memname, mem in inspect.getmembers(sys.modules["lldb_rb.rb_base_command"]): + if inspect.isclass(mem): + 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 debugger.HandleCommand("command script add -f lldb_cruby.lldb_rp rp") 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") diff --git a/misc/rb_base_command.py b/misc/rb_base_command.py new file mode 100644 index 0000000000..44b2996d80 --- /dev/null +++ b/misc/rb_base_command.py @@ -0,0 +1,68 @@ +import lldb +from pydoc import locate + +class RbBaseCommand: + @classmethod + def register_lldb_command(cls, debugger, module_name): + # Add any commands contained in this module to LLDB + command = f"command script add -c {module_name}.{cls.__name__} {cls.program}" + debugger.HandleCommand(command) + + def __init__(self, debugger, _internal_dict): + self.internal_dict = _internal_dict + + def __call__(self, debugger, command, exe_ctx, result): + if not ("RUBY_Qfalse" in globals()): + self._lldb_init(debugger) + + self.build_environment(debugger) + self.call(debugger, command, exe_ctx, result) + + def call(self, debugger, command, exe_ctx, result): + raise NotImplementedError("subclasses must implement call") + + def get_short_help(self): + return self.__class__.help_string + + def get_long_help(self): + return self.__class__.help_string + + def build_environment(self, debugger): + self.target = debugger.GetSelectedTarget() + self.process = self.target.GetProcess() + self.thread = self.process.GetSelectedThread() + self.frame = self.thread.GetSelectedFrame() + + def _append_command_output(self, debugger, command, result): + output1 = result.GetOutput() + debugger.GetCommandInterpreter().HandleCommand(command, result) + output2 = result.GetOutput() + result.Clear() + result.write(output1) + result.write(output2) + + def _lldb_init(self, debugger): + target = debugger.GetSelectedTarget() + global SIZEOF_VALUE + SIZEOF_VALUE = target.FindFirstType("VALUE").GetByteSize() + + value_types = [] + g = globals() + + imemo_types = target.FindFirstType("enum imemo_type") + + for member in imemo_types.GetEnumMembers(): + g[member.GetName()] = member.GetValueAsUnsigned() + + 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() + g[name] = value + + if name.startswith("RUBY_T_"): + value_types.append(name) + g["value_types"] = value_types |