diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt --- a/lldb/bindings/python/CMakeLists.txt +++ b/lldb/bindings/python/CMakeLists.txt @@ -104,6 +104,13 @@ FILES "${LLDB_SOURCE_DIR}/examples/python/in_call_stack.py" "${LLDB_SOURCE_DIR}/examples/python/symbolication.py") + create_python_package( + ${swig_target} + ${lldb_python_target_dir} + "plugins" + FILES + "${LLDB_SOURCE_DIR}/examples/python/scripted_process/scripted_process.py") + if(APPLE) create_python_package( ${swig_target} diff --git a/lldb/examples/python/scripted_process/my_scripted_process.py b/lldb/examples/python/scripted_process/my_scripted_process.py new file mode 100644 --- /dev/null +++ b/lldb/examples/python/scripted_process/my_scripted_process.py @@ -0,0 +1,42 @@ +import os + +import lldb +from lldb.plugins.scripted_process import ScriptedProcess + +class MyScriptedProcess(ScriptedProcess): + def __init__(self, target: lldb.SBTarget, args : lldb.SBStructuredData): + super().__init__(target, args) + + def get_memory_region_containing_address(self, addr: int) -> lldb.SBMemoryRegionInfo: + return self.memory_regions[0] + + def get_thread_with_id(self, tid: int): + return {} + + def get_registers_for_thread(self, tid: int): + return {} + + def read_memory_at_address(self, addr: int, size: int) -> lldb.SBData: + data = lldb.SBData().CreateDataFromCString( + self.target.GetByteOrder(), + self.target.GetCodeByteSize(), + "Hello, world!") + return data + + def get_loaded_images(self): + return self.loaded_images + + def get_process_id(self) -> int: + return 42 + + def is_alive(self) -> bool: + return True + +def __lldb_init_module(debugger, dict): + if not 'SKIP_SCRIPTED_PROCESS_LAUNCH' in os.environ: + debugger.HandleCommand( + "process launch -C %s.%s" % (__name__, + MyScriptedProcess.__name__)) + else: + print("Name of the class that will manage the scripted process: '%s.%s'" + % (__name__, MyScriptedProcess.__name__)) \ No newline at end of file diff --git a/lldb/examples/python/scripted_process/scripted_process.py b/lldb/examples/python/scripted_process/scripted_process.py new file mode 100644 --- /dev/null +++ b/lldb/examples/python/scripted_process/scripted_process.py @@ -0,0 +1,179 @@ +from abc import ABCMeta, abstractmethod +import six + +import lldb + +@six.add_metaclass(ABCMeta) +class ScriptedProcess: + + """ + The base class for a scripted process. + + Most of the base class methods are `@abstractmethod` that need to be + overwritten by the inheriting class. + + DISCLAIMER: THIS INTERFACE IS STILL UNDER DEVELOPMENT AND NOT STABLE. + THE METHODS EXPOSED MIGHT CHANGE IN THE FUTURE. + + args (lldb.SBStructuredData): Dictionary holding arbitrary values + target (lldb.SBTarget): Target launching the scripted process. + + get_memory_region_containing_address(addr: int) -> lldb.SBMemoryRegionInfo: + Get the memory region for the scripted process, containing a + specific address. + + get_thread_with_id(tid: int) -> Dict: + Get the scripted process thread with a specific ID. + + get_registers_for_thread(tid:int) -> Dict: + Get the register context dictionary for a certain thread. + + read_memory_at_address(addr:int, size:int) -> lldb.SBData: + Get a memory buffer from the scripted process at a certain address, + of a certain size. + + get_loaded_images() -> List: + Get the list of loaded images for the scripted process. + + get_process_id() -> int: + Get the scripted process identifier. + + launch() -> lldb.SBError: + Simulate the scripted process launch. + + resume() -> lldb.SBError: + Simulate the scripted process resume. + + is_alive() -> bool: + Check if the scripted process is alive. + """ + + @abstractmethod + def __init__(self, target, args): + """ Construct a scripted process. + + Args: + target (lldb.SBTarget): The target launching the scripted process. + args (lldb.SBStructuredData): A Dictionary holding arbitrary + key/value pairs used by the scripted process. + """ + self.target = None + self.args = None + if isinstance(target, lldb.SBTarget) and target.IsValid(): + self.target = target + if isinstance(args, lldb.SBStructuredData) and args.IsValid(): + self.args = args + + @abstractmethod + def get_memory_region_containing_address(addr): + """ Get the memory region for the scripted process, containing a + specific address. + + Args: + addr (int): Address to look for in the scripted process memory + regions. + + Returns: + lldb.SBMemoryRegionInfo: The memory region containing the address. + None if out of bounds. + """ + pass + + @abstractmethod + def get_thread_with_id(tid): + """ Get the scripted process thread with a specific ID. + + Args: + tid (int): Thread ID to look for in the scripted process. + + Returns: + Dict: The thread represented as a dictionary, withr the + tid thread ID. None if tid doesn't match any of the scripted + process threads. + """ + pass + + @abstractmethod + def get_registers_for_thread(tid): + """ Get the register context dictionary for a certain thread of + the scripted process. + + Args: + tid (int): Thread ID for the thread's register context. + + Returns: + Dict: The register context represented as a dictionary, for the + tid thread. None if tid doesn't match any of the scripted + process threads. + """ + pass + + @abstractmethod + def read_memory_at_address(addr, size): + """ Get a memory buffer from the scripted process at a certain address, + of a certain size. + + Args: + addr (int): Address from which we should start reading. + size (int): Size of the memory to read. + + Returns: + lldb.SBData: An `lldb.SBData` buffer with the target byte size and + byte order storing the memory read. + """ + pass + + @abstractmethod + def get_loaded_images(self): + """ Get the list of loaded images for the scripted process. + + ``` + class ScriptedProcessImage: + def __init__(name, file_spec, uuid, load_address): + self.name = name + self.file_spec = file_spec + self.uuid = uuid + self.load_address = load_address + ``` + + Returns: + List[ScriptedProcessImage]: A list of `ScriptedProcessImage` + containing for each entry, the name of the library, a UUID, + an `lldb.SBFileSpec` and a load address. + None if the list is empty. + """ + pass + + def get_process_id(self): + """ Get the scripted process identifier. + + Returns: + int: The scripted process identifier. + """ + return 0 + + + def launch(self): + """ Simulate the scripted process launch. + + Returns: + lldb.SBError: An `lldb.SBError` with error code 0. + """ + return lldb.SBError() + + def resume(self): + """ Simulate the scripted process resume. + + Returns: + lldb.SBError: An `lldb.SBError` with error code 0. + """ + return lldb.SBError() + + @abstractmethod + def is_alive(self): + """ Check if the scripted process is alive. + + Returns: + bool: True if scripted process is alive. False otherwise. + """ + pass diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h --- a/lldb/include/lldb/Target/Process.h +++ b/lldb/include/lldb/Target/Process.h @@ -2561,8 +2561,6 @@ virtual size_t DoReadMemory(lldb::addr_t vm_addr, void *buf, size_t size, Status &error) = 0; - void SetState(lldb::EventSP &event_sp); - lldb::StateType GetPrivateState(); /// The "private" side of resuming a process. This doesn't alter the state diff --git a/lldb/source/Plugins/Process/CMakeLists.txt b/lldb/source/Plugins/Process/CMakeLists.txt --- a/lldb/source/Plugins/Process/CMakeLists.txt +++ b/lldb/source/Plugins/Process/CMakeLists.txt @@ -12,6 +12,7 @@ elseif (CMAKE_SYSTEM_NAME MATCHES "Darwin") add_subdirectory(MacOSX-Kernel) endif() +add_subdirectory(scripted) add_subdirectory(gdb-remote) add_subdirectory(Utility) add_subdirectory(elf-core) diff --git a/lldb/source/Plugins/Process/scripted/CMakeLists.txt b/lldb/source/Plugins/Process/scripted/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/CMakeLists.txt @@ -0,0 +1,13 @@ +add_lldb_library(lldbPluginScriptedProcess PLUGIN + ScriptedProcess.cpp + + LINK_LIBS + lldbCore + lldbTarget + lldbUtility + lldbPluginProcessUtility + LINK_COMPONENTS + BinaryFormat + Object + Support + ) diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h @@ -0,0 +1,111 @@ +//===-- ScriptedProcess.h ------------------------------------- -*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H +#define LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H + +#include "lldb/Target/Process.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/Status.h" + +namespace lldb_private { + +class ScriptedProcess : public Process { +protected: + class LaunchInfo { + public: + LaunchInfo(const ProcessLaunchInfo &launch_info) { + m_class_name = launch_info.GetScriptedProcessClassName(); + m_dictionary_sp = launch_info.GetScriptedProcessDictionarySP(); + } + + std::string GetClassName() const { return m_class_name; } + StructuredData::DictionarySP GetDictionarySP() const { + return m_dictionary_sp; + } + + private: + std::string m_class_name; + StructuredData::DictionarySP m_dictionary_sp; + }; + +public: + static lldb::ProcessSP CreateInstance(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const FileSpec *crash_file_path, + bool can_connect); + + static void Initialize(); + + static void Terminate(); + + static ConstString GetPluginNameStatic(); + + static const char *GetPluginDescriptionStatic(); + + ScriptedProcess(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp, + const ScriptedProcess::LaunchInfo &launch_info); + + ~ScriptedProcess() override; + + bool CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) override; + + DynamicLoader *GetDynamicLoader() override { return nullptr; } + + ConstString GetPluginName() override; + + uint32_t GetPluginVersion() override; + + SystemRuntime *GetSystemRuntime() override { return nullptr; } + + Status DoLaunch(Module *exe_module, ProcessLaunchInfo &launch_info) override; + + void DidLaunch() override; + + Status DoResume() override; + + Status DoDestroy() override; + + void RefreshStateAfterStop() override{}; + + bool IsAlive() override; + + size_t ReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) override; + + size_t DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) override; + + ArchSpec GetArchitecture(); + + Status GetMemoryRegionInfo(lldb::addr_t load_addr, + MemoryRegionInfo &range_info) override; + + Status + GetMemoryRegions(lldb_private::MemoryRegionInfos ®ion_list) override; + + bool GetProcessInfo(ProcessInstanceInfo &info) override; + +protected: + void Clear(); + + bool DoUpdateThreadList(ThreadList &old_thread_list, + ThreadList &new_thread_list) override; + +private: + ScriptedProcessInterface &GetInterface() const; + + const LaunchInfo m_launch_info; + lldb_private::ScriptInterpreter *m_interpreter; + lldb_private::StructuredData::ObjectSP m_python_object_sp; +}; + +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_SCRIPTED_PROCESS_H diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp @@ -0,0 +1,229 @@ +//===-- ScriptedProcess.cpp -----------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "ScriptedProcess.h" + +#include "lldb/Core/Debugger.h" +#include "lldb/Core/Module.h" +#include "lldb/Core/PluginManager.h" + +#include "lldb/Host/OptionParser.h" + +#include "lldb/Interpreter/OptionArgParser.h" +#include "lldb/Interpreter/OptionGroupBoolean.h" +#include "lldb/Interpreter/ScriptInterpreter.h" +#include "lldb/Target/MemoryRegionInfo.h" + +LLDB_PLUGIN_DEFINE(ScriptedProcess) + +using namespace lldb; +using namespace lldb_private; + +ConstString ScriptedProcess::GetPluginNameStatic() { + static ConstString g_name("ScriptedProcess"); + return g_name; +} + +const char *ScriptedProcess::GetPluginDescriptionStatic() { + return "Scripted Process plug-in."; +} + +lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const FileSpec *file, + bool can_connect) { + ScriptedProcess::LaunchInfo launch_info(target_sp->GetProcessLaunchInfo()); + + return std::make_shared(target_sp, listener_sp, launch_info); +} + +bool ScriptedProcess::CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) { + return true; +} + +ScriptedProcess::ScriptedProcess(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const ScriptedProcess::LaunchInfo &launch_info) + : Process(target_sp, listener_sp), m_launch_info(launch_info), + m_interpreter(nullptr), m_python_object_sp(nullptr) { + if (!target_sp) + return; + + m_interpreter = target_sp->GetDebugger().GetScriptInterpreter(); + + if (!m_interpreter) + return; + + StructuredData::ObjectSP object_sp = GetInterface().CreatePluginObject( + m_launch_info.GetClassName().c_str(), target_sp, + m_launch_info.GetDictionarySP()); + + if (object_sp && object_sp->IsValid()) + m_python_object_sp = object_sp; +} + +ScriptedProcess::~ScriptedProcess() { + Clear(); + // We need to call finalize on the process before destroying ourselves to + // make sure all of the broadcaster cleanup goes as planned. If we destruct + // this class, then Process::~Process() might have problems trying to fully + // destroy the broadcaster. + Finalize(); +} + +void ScriptedProcess::Initialize() { + static llvm::once_flag g_once_flag; + + llvm::call_once(g_once_flag, []() { + PluginManager::RegisterPlugin(GetPluginNameStatic(), + GetPluginDescriptionStatic(), CreateInstance); + }); +} + +void ScriptedProcess::Terminate() { + PluginManager::UnregisterPlugin(ScriptedProcess::CreateInstance); +} + +ConstString ScriptedProcess::GetPluginName() { return GetPluginNameStatic(); } + +uint32_t ScriptedProcess::GetPluginVersion() { return 1; } + +Status ScriptedProcess::DoLaunch(Module *exe_module, + ProcessLaunchInfo &launch_info) { + if (!m_interpreter) + return Status("No interpreter."); + + if (!m_python_object_sp) + return Status("No python object."); + + Status status = GetInterface().Launch(); + + if (status.Success()) { + SetPrivateState(eStateRunning); + SetPrivateState(eStateStopped); + } + + return status; +}; + +void ScriptedProcess::DidLaunch() { + if (m_interpreter) + m_pid = GetInterface().GetProcessID(); +} + +Status ScriptedProcess::DoResume() { + if (!m_interpreter) + return Status("No interpreter."); + + if (!m_python_object_sp) + return Status("No python object."); + + Status status = GetInterface().Resume(); + + if (status.Success()) { + SetPrivateState(eStateRunning); + SetPrivateState(eStateStopped); + } + + return status; +} + +Status ScriptedProcess::DoDestroy() { return Status(); } + +bool ScriptedProcess::IsAlive() { + if (!m_interpreter) + return false; + + return GetInterface().IsAlive(); +} + +size_t ScriptedProcess::ReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) { + return DoReadMemory(addr, buf, size, error); +} + +size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) { + + auto error_with_message = [&error](llvm::StringRef message) { + error.SetErrorString(message); + return LLDB_INVALID_ADDRESS; + }; + + if (!m_interpreter) + return error_with_message("No interpreter."); + + lldb::DataExtractorSP data_extractor_sp = + GetInterface().ReadMemoryAtAddress(addr, size, error); + + if (!data_extractor_sp || error.Fail()) + return LLDB_INVALID_ADDRESS; + + if (data_extractor_sp->GetByteSize() != size) + return error_with_message("Failed to read requested memory size."); + + if (data_extractor_sp->CopyData(0, size, buf) <= size) + return error_with_message("Failed to copy read memory to buffer."); + + return size; +} + +ArchSpec ScriptedProcess::GetArchitecture() { + return GetTarget().GetArchitecture(); +} + +Status ScriptedProcess::GetMemoryRegionInfo(lldb::addr_t load_addr, + MemoryRegionInfo ®ion) { + return Status(); +} + +Status ScriptedProcess::GetMemoryRegions(MemoryRegionInfos ®ion_list) { + Status error; + + if (!m_interpreter) { + error.SetErrorString("No interpreter."); + return error; + } + + lldb::addr_t address = 0; + lldb::MemoryRegionInfoSP mem_region_sp = nullptr; + + while ((mem_region_sp = + GetInterface().GetMemoryRegionContainingAddress(address))) { + auto range = mem_region_sp->GetRange(); + address += range.GetRangeBase() + range.GetByteSize(); + region_list.push_back(*mem_region_sp.get()); + } + + return error; +} + +void ScriptedProcess::Clear() { Process::m_thread_list.Clear(); } + +bool ScriptedProcess::DoUpdateThreadList(ThreadList &old_thread_list, + ThreadList &new_thread_list) { + return new_thread_list.GetSize(false) > 0; +} + +bool ScriptedProcess::GetProcessInfo(ProcessInstanceInfo &info) { + info.Clear(); + info.SetProcessID(GetID()); + info.SetArchitecture(GetArchitecture()); + lldb::ModuleSP module_sp = GetTarget().GetExecutableModule(); + if (module_sp) { + const bool add_exe_file_as_first_arg = false; + info.SetExecutableFile(GetTarget().GetExecutableModule()->GetFileSpec(), + add_exe_file_as_first_arg); + } + return true; +} + +ScriptedProcessInterface &ScriptedProcess::GetInterface() const { + return m_interpreter->GetScriptedProcessInterface(); +} diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -2950,7 +2950,7 @@ // If we're not already connected to the process, and if we have a platform // that can launch a process for debugging, go ahead and do that here. if (state != eStateConnected && platform_sp && - platform_sp->CanDebugProcess()) { + platform_sp->CanDebugProcess() && !launch_info.IsScriptedProcess()) { LLDB_LOGF(log, "Target::%s asking the platform to debug the process", __FUNCTION__); diff --git a/lldb/test/API/functionalities/scripted_process/Makefile b/lldb/test/API/functionalities/scripted_process/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/scripted_process/Makefile @@ -0,0 +1,4 @@ +C_SOURCES := main.c + +include Makefile.rules + diff --git a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py @@ -0,0 +1,67 @@ +""" +Test python scripted process in lldb +""" + +import os + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +from lldbsuite.test import lldbtest + + +class PlatformProcessCrashInfoTestCase(TestBase): + + mydir = TestBase.compute_mydir(__file__) + + def setUp(self): + TestBase.setUp(self) + self.source = "main.c" + + def tearDown(self): + TestBase.tearDown(self) + + def test_python_plugin_package(self): + """Test that the lldb python module has a `plugins.scripted_process` + package.""" + self.expect('script import lldb.plugins', + substrs=["ModuleNotFoundError"], matching=False) + + self.expect('script dir(lldb.plugins)', + substrs=["scripted_process"]) + + self.expect('script import lldb.plugins.scripted_process', + substrs=["ModuleNotFoundError"], matching=False) + + self.expect('script dir(lldb.plugins.scripted_process)', + substrs=["ScriptedProcess"]) + + self.expect('script from lldb.plugins.scripted_process import ScriptedProcess', + substrs=["ImportError"], matching=False) + + self.expect('script dir(ScriptedProcess)', + substrs=["launch"]) + + + + def test_launch_scripted_process(self): + """Test that we can launch an lldb scripted process and check its + process ID """ + self.build() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target, VALID_TARGET) + + scripted_process_example_relpath = ['..','..','..','..','examples','python','scripted_process','my_scripted_process.py'] + os.environ['SKIP_SCRIPTED_PROCESS_LAUNCH'] = '1' + self.runCmd("command script import " + os.path.join(self.getSourceDir(), + scripted_process_example_relpath)) + + launch_info = lldb.SBLaunchInfo(None) + launch_info.SetProcessPluginName('ScriptedProcess') + launch_info.SetScriptedProcessClassName('my_scripted_process.MyScriptedProcess') + + error = lldb.SBError() + process = target.Launch(launch_info, error) + self.assertTrue(process, PROCESS_IS_VALID) + self.assertEqual(process.GetProcessID(), 42) diff --git a/lldb/test/API/functionalities/scripted_process/main.c b/lldb/test/API/functionalities/scripted_process/main.c new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/scripted_process/main.c @@ -0,0 +1,5 @@ +#include + +int main() { + return 0; // break here +}