diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h --- a/lldb/include/lldb/Target/Target.h +++ b/lldb/include/lldb/Target/Target.h @@ -1105,6 +1105,37 @@ void ClearAllLoadedSections(); + /// Set the \a Trace object containing processor trace information of this + /// target. + /// + /// \param[in] trace_sp + /// The trace object. + void SetTrace(const lldb::TraceSP &trace_sp); + + /// Get the \a Trace object containing processor trace information of this + /// target. + /// + /// \return + /// The trace object. It might be undefined. + lldb::TraceSP &GetTrace(); + + /// Dump the instructions of the given thread's \a Trace. + /// + /// \param[in] s + /// The stream object where the instructions are printed. + /// + /// \param[in] tid + /// The thread to print the instruction of. + /// + /// \param[in] count + /// The number of instructions to print. + /// + /// \param[in] offset + /// Offset from the last instruction of the trace to start displaying + /// instructions from. + void DumpTraceInstructions(Stream &s, lldb::tid_t tid, size_t count, + size_t offset = 0) const; + // Since expressions results can persist beyond the lifetime of a process, // and the const expression results are available after a process is gone, we // provide a way for expressions to be evaluated from the Target itself. If @@ -1394,6 +1425,9 @@ bool m_suppress_stop_hooks; bool m_is_dummy_target; unsigned m_next_persistent_variable_index = 0; + /// An optional \a lldb_private::Trace object containing processor trace + /// information of this target. + lldb::TraceSP m_trace_sp; /// Stores the frame recognizers of this target. lldb::StackFrameRecognizerManagerUP m_frame_recognizer_manager_up; diff --git a/lldb/include/lldb/Target/Trace.h b/lldb/include/lldb/Target/Trace.h --- a/lldb/include/lldb/Target/Trace.h +++ b/lldb/include/lldb/Target/Trace.h @@ -33,7 +33,8 @@ /// Processor trace information can also be fetched through the process /// interfaces during a live debug session if your process supports gathering /// this information. -class Trace : public PluginInterface { +class Trace : public PluginInterface, + public std::enable_shared_from_this { public: /// Dump the trace data that this plug-in has access to. /// @@ -128,14 +129,9 @@ protected: friend class TraceSettingsParser; - /// JSON object that holds all settings for this trace session. - llvm::json::Object m_settings; - /// The directory that contains the settings file. - std::string m_settings_dir; std::map> m_thread_to_trace_file_map; - std::vector m_targets; }; } // namespace lldb_private diff --git a/lldb/source/Commands/CommandObjectThread.cpp b/lldb/source/Commands/CommandObjectThread.cpp --- a/lldb/source/Commands/CommandObjectThread.cpp +++ b/lldb/source/Commands/CommandObjectThread.cpp @@ -2165,6 +2165,128 @@ ~CommandObjectMultiwordThreadPlan() override = default; }; +// Next are the subcommands of CommandObjectMultiwordTrace + +// CommandObjectTraceDumpInstructions +#define LLDB_OPTIONS_thread_trace_dump_instructions +#include "CommandOptions.inc" + +class CommandObjectTraceDumpInstructions + : public CommandObjectIterateOverThreads { +public: + class CommandOptions : public Options { + public: + CommandOptions() : Options() { OptionParsingStarting(nullptr); } + + ~CommandOptions() override = default; + + Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, + ExecutionContext *execution_context) override { + Status error; + const int short_option = m_getopt_table[option_idx].val; + + switch (short_option) { + case 'c': { + int32_t count; + if (option_arg.empty() || option_arg.getAsInteger(0, count) || + count < 0) + error.SetErrorStringWithFormat( + "invalid integer value for option '%s'", + option_arg.str().c_str()); + else + m_count = count; + break; + } + case 'o': { + int32_t offset; + if (option_arg.empty() || option_arg.getAsInteger(0, offset) || + offset < 0) + error.SetErrorStringWithFormat( + "invalid integer value for option '%s'", + option_arg.str().c_str()); + else + m_offset = offset; + break; + } + default: + llvm_unreachable("Unimplemented option"); + } + return error; + } + + void OptionParsingStarting(ExecutionContext *execution_context) override { + m_count = 10; + m_offset = 0; + } + + llvm::ArrayRef GetDefinitions() override { + return llvm::makeArrayRef(g_thread_trace_dump_instructions_options); + } + + // Instance variables to hold the values for command options. + uint32_t m_count; + uint32_t m_offset; + }; + + CommandObjectTraceDumpInstructions(CommandInterpreter &interpreter) + : CommandObjectIterateOverThreads( + interpreter, "thread trace dump instructions", + "Dump the traced instructions for one or more threads. If no " + "threads are specified, show the current thread. Use the " + "thread-index \"all\" to see all threads.", + nullptr, + eCommandRequiresProcess | eCommandTryTargetAPILock | + eCommandProcessMustBeLaunched | eCommandProcessMustBePaused), + m_options() {} + + ~CommandObjectTraceDumpInstructions() override = default; + + Options *GetOptions() override { return &m_options; } + +protected: + bool HandleOneThread(lldb::tid_t tid, CommandReturnObject &result) override { + Target *target = m_exe_ctx.GetTargetPtr(); + + target->DumpTraceInstructions(result.GetOutputStream(), tid, + m_options.m_count, m_options.m_offset); + return true; + } + + CommandOptions m_options; +}; + +// CommandObjectMultiwordTraceDump +class CommandObjectMultiwordTraceDump : public CommandObjectMultiword { +public: + CommandObjectMultiwordTraceDump(CommandInterpreter &interpreter) + : CommandObjectMultiword( + interpreter, "dump", + "Commands for displaying trace information of the threads " + "in the current process.", + "thread trace dump []") { + LoadSubCommand( + "instructions", + CommandObjectSP(new CommandObjectTraceDumpInstructions(interpreter))); + } + ~CommandObjectMultiwordTraceDump() override = default; +}; + +// CommandObjectMultiwordTrace +class CommandObjectMultiwordTrace : public CommandObjectMultiword { +public: + CommandObjectMultiwordTrace(CommandInterpreter &interpreter) + : CommandObjectMultiword( + interpreter, "trace", + "Commands for operating on traces of the threads in the current " + "process.", + "thread trace []") { + LoadSubCommand("dump", CommandObjectSP(new CommandObjectMultiwordTraceDump( + interpreter))); + } + + ~CommandObjectMultiwordTrace() override = default; +}; + // CommandObjectMultiwordThread CommandObjectMultiwordThread::CommandObjectMultiwordThread( @@ -2240,6 +2362,8 @@ LoadSubCommand("plan", CommandObjectSP(new CommandObjectMultiwordThreadPlan( interpreter))); + LoadSubCommand("trace", + CommandObjectSP(new CommandObjectMultiwordTrace(interpreter))); } CommandObjectMultiwordThread::~CommandObjectMultiwordThread() = default; diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td --- a/lldb/source/Commands/Options.td +++ b/lldb/source/Commands/Options.td @@ -1005,6 +1005,16 @@ Desc<"Display thread plans for unreported threads">; } +let Command = "thread trace dump instructions" in { + def thread_trace_dump_instructions_count : Option<"count", "c">, Group<1>, + Arg<"Count">, + Desc<"The number of total instructions to display.">; + def thread_trace_dump_instructions_offset: Option<"offset", "o">, Group<1>, + Arg<"Offset">, + Desc<"Offset from the last instruction of the trace to start displaying " + "instructions from. Defaults to 0.">; +} + let Command = "type summary add" in { def type_summary_add_category : Option<"category", "w">, Arg<"Name">, Desc<"Add this to the given category instead of the default one.">; 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 @@ -17,3 +17,4 @@ add_subdirectory(elf-core) add_subdirectory(mach-core) add_subdirectory(minidump) +add_subdirectory(Trace) diff --git a/lldb/source/Plugins/Process/Trace/CMakeLists.txt b/lldb/source/Plugins/Process/Trace/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/Trace/CMakeLists.txt @@ -0,0 +1,13 @@ +add_lldb_library(lldbPluginProcessTrace PLUGIN + ProcessTrace.cpp + + LINK_LIBS + lldbCore + lldbTarget + lldbUtility + lldbPluginProcessUtility + LINK_COMPONENTS + BinaryFormat + Object + Support + ) diff --git a/lldb/source/Plugins/Process/Trace/ProcessTrace.h b/lldb/source/Plugins/Process/Trace/ProcessTrace.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/Trace/ProcessTrace.h @@ -0,0 +1,82 @@ +//===-- ProcessTrace.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_PROCESS_TRACE_PROCESSTRACE_H +#define LLDB_SOURCE_PLUGINS_PROCESS_TRACE_PROCESSTRACE_H + +#include "lldb/Target/Process.h" +#include "lldb/Utility/ConstString.h" +#include "lldb/Utility/Status.h" + +struct ThreadData; + +class ProcessTrace : public lldb_private::Process { +public: + static lldb::ProcessSP + CreateInstance(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp, + const lldb_private::FileSpec *crash_file_path); + + static void Initialize(); + + static void Terminate(); + + static lldb_private::ConstString GetPluginNameStatic(); + + static const char *GetPluginDescriptionStatic(); + + ProcessTrace(lldb::TargetSP target_sp, lldb::ListenerSP listener_sp); + + ~ProcessTrace() override; + + bool CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) override; + + lldb_private::Status DoLoadCore() override; + + lldb_private::DynamicLoader *GetDynamicLoader() override { return nullptr; } + + lldb_private::SystemRuntime *GetSystemRuntime() override { return nullptr; } + + lldb_private::ConstString GetPluginName() override; + + uint32_t GetPluginVersion() override; + + lldb_private::Status DoDestroy() override; + + void RefreshStateAfterStop() override; + + lldb_private::Status WillResume() override { + lldb_private::Status error; + error.SetErrorStringWithFormat( + "error: %s does not support resuming processes", + GetPluginName().GetCString()); + return error; + } + + bool IsAlive() override; + + bool WarnBeforeDetach() const override { return false; } + + size_t ReadMemory(lldb::addr_t addr, void *buf, size_t size, + lldb_private::Status &error) override; + + size_t DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + lldb_private::Status &error) override; + + lldb_private::ArchSpec GetArchitecture(); + + bool GetProcessInfo(lldb_private::ProcessInstanceInfo &info) override; + +protected: + void Clear(); + + bool UpdateThreadList(lldb_private::ThreadList &old_thread_list, + lldb_private::ThreadList &new_thread_list) override; +}; + +#endif // LLDB_SOURCE_PLUGINS_PROCESS_TRACE_PROCESSTRACE_H diff --git a/lldb/source/Plugins/Process/Trace/ProcessTrace.cpp b/lldb/source/Plugins/Process/Trace/ProcessTrace.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Process/Trace/ProcessTrace.cpp @@ -0,0 +1,116 @@ +//===-- ProcessTrace.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 "ProcessTrace.h" + +#include + +#include "lldb/Core/Module.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Target/Target.h" + +using namespace lldb_private; + +LLDB_PLUGIN_DEFINE(ProcessTrace) + +ConstString ProcessTrace::GetPluginNameStatic() { + static ConstString g_name("trace"); + return g_name; +} + +const char *ProcessTrace::GetPluginDescriptionStatic() { + return "Trace process plug-in."; +} + +void ProcessTrace::Terminate() { + PluginManager::UnregisterPlugin(ProcessTrace::CreateInstance); +} + +lldb::ProcessSP ProcessTrace::CreateInstance(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp, + const FileSpec *crash_file) { + return std::make_shared(target_sp, listener_sp); +} + +bool ProcessTrace::CanDebug(lldb::TargetSP target_sp, + bool plugin_specified_by_name) { + return true; +} + +ProcessTrace::ProcessTrace(lldb::TargetSP target_sp, + lldb::ListenerSP listener_sp) + : Process(target_sp, listener_sp) {} + +ProcessTrace::~ProcessTrace() { + 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(); +} + +ConstString ProcessTrace::GetPluginName() { return GetPluginNameStatic(); } + +uint32_t ProcessTrace::GetPluginVersion() { return 1; } + +Status ProcessTrace::DoLoadCore() { + SetCanJIT(false); + return Status(); +} + +bool ProcessTrace::UpdateThreadList(ThreadList &old_thread_list, + ThreadList &new_thread_list) { + return false; +} + +void ProcessTrace::RefreshStateAfterStop() {} + +Status ProcessTrace::DoDestroy() { return Status(); } + +bool ProcessTrace::IsAlive() { return true; } + +size_t ProcessTrace::ReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) { + // Don't allow the caching that lldb_private::Process::ReadMemory does since + // we have it all cached in the trace files. + return DoReadMemory(addr, buf, size, error); +} + +void ProcessTrace::Clear() { m_thread_list.Clear(); } + +void ProcessTrace::Initialize() { + static llvm::once_flag g_once_flag; + + llvm::call_once(g_once_flag, []() { + PluginManager::RegisterPlugin(GetPluginNameStatic(), + GetPluginDescriptionStatic(), CreateInstance); + }); +} + +ArchSpec ProcessTrace::GetArchitecture() { + return GetTarget().GetArchitecture(); +} + +bool ProcessTrace::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; +} + +size_t ProcessTrace::DoReadMemory(lldb::addr_t addr, void *buf, size_t size, + Status &error) { + return 0; +} diff --git a/lldb/source/Plugins/Process/Utility/HistoryThread.h b/lldb/source/Plugins/Process/Utility/HistoryThread.h --- a/lldb/source/Plugins/Process/Utility/HistoryThread.h +++ b/lldb/source/Plugins/Process/Utility/HistoryThread.h @@ -34,7 +34,8 @@ public: HistoryThread(lldb_private::Process &process, lldb::tid_t tid, std::vector pcs, - bool pcs_are_call_addresses = false); + bool pcs_are_call_addresses = false, + bool use_invalid_index_id = true); ~HistoryThread() override; diff --git a/lldb/source/Plugins/Process/Utility/HistoryThread.cpp b/lldb/source/Plugins/Process/Utility/HistoryThread.cpp --- a/lldb/source/Plugins/Process/Utility/HistoryThread.cpp +++ b/lldb/source/Plugins/Process/Utility/HistoryThread.cpp @@ -26,10 +26,11 @@ HistoryThread::HistoryThread(lldb_private::Process &process, lldb::tid_t tid, std::vector pcs, - bool pcs_are_call_addresses) - : Thread(process, tid, true), m_framelist_mutex(), m_framelist(), - m_pcs(pcs), m_extended_unwind_token(LLDB_INVALID_ADDRESS), m_queue_name(), - m_thread_name(), m_originating_unique_thread_id(tid), + bool pcs_are_call_addresses, + bool use_invalid_index_id) + : Thread(process, tid, use_invalid_index_id), m_framelist_mutex(), + m_framelist(), m_pcs(pcs), m_extended_unwind_token(LLDB_INVALID_ADDRESS), + m_queue_name(), m_thread_name(), m_originating_unique_thread_id(tid), m_queue_id(LLDB_INVALID_QUEUE_ID) { m_unwinder_up = std::make_unique(*this, pcs, pcs_are_call_addresses); 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 @@ -2965,6 +2965,20 @@ return error; } +void Target::SetTrace(const lldb::TraceSP &trace_sp) { m_trace_sp = trace_sp; } + +lldb::TraceSP &Target::GetTrace() { return m_trace_sp; } + +void Target::DumpTraceInstructions(Stream &s, lldb::tid_t tid, size_t count, + size_t offset) const { + if (!m_trace_sp) { + s << "error: no trace in this target"; + return; + } + s.Printf("placeholder trace of tid %" PRIu64 ", count = %zu, offset = %zu\n", + tid, count, offset); +} + Status Target::Attach(ProcessAttachInfo &attach_info, Stream *stream) { auto state = eStateInvalid; auto process_sp = GetProcessSP(); diff --git a/lldb/source/Target/Trace.cpp b/lldb/source/Target/Trace.cpp --- a/lldb/source/Target/Trace.cpp +++ b/lldb/source/Target/Trace.cpp @@ -85,11 +85,7 @@ llvm::Error Trace::ParseSettings(Debugger &debugger, const llvm::json::Value &settings, llvm::StringRef settings_dir) { - if (llvm::Error err = - CreateParser()->ParseSettings(debugger, settings, settings_dir)) - return err; - - return llvm::Error::success(); + return CreateParser()->ParseSettings(debugger, settings, settings_dir); } llvm::StringRef Trace::GetSchema() { return CreateParser()->GetSchema(); } diff --git a/lldb/source/Target/TraceSettingsParser.cpp b/lldb/source/Target/TraceSettingsParser.cpp --- a/lldb/source/Target/TraceSettingsParser.cpp +++ b/lldb/source/Target/TraceSettingsParser.cpp @@ -65,7 +65,12 @@ NormalizePath(spec); m_thread_to_trace_file_map[process_sp->GetID()][tid] = spec; - ThreadSP thread_sp(new HistoryThread(*process_sp, tid, /*callstack*/ {})); + // For now we are setting an invalid PC. Eventually we should be able to + // lazyly reconstruct this callstack after decoding the trace. + ThreadSP thread_sp(new HistoryThread(*process_sp, tid, + /*callstack*/ {LLDB_INVALID_ADDRESS}, + /*pcs_are_call_addresses*/ false, + /*use_invalid_index_id*/ false)); process_sp->GetThreadList().AddThread(thread_sp); } @@ -107,8 +112,7 @@ debugger.GetTargetList().SetSelectedTarget(target_sp.get()); ProcessSP process_sp(target_sp->CreateProcess( - /*listener*/ nullptr, /*plugin_name*/ llvm::StringRef(), - /*crash_file*/ nullptr)); + /*listener*/ nullptr, /*plugin_name*/ "trace", /*crash_file*/ nullptr)); process_sp->SetID(static_cast(process.pid)); for (const JSONThread &thread : process.threads) @@ -118,6 +122,15 @@ if (llvm::Error err = ParseModule(target_sp, module)) return err; } + if (!process.threads.empty()) + process_sp->GetThreadList().SetSelectedThreadByIndexID(0); + + // We invoke LoadCore to create a correct stopped state for the process and + // its threads + error = process_sp->LoadCore(); + if (error.Fail()) + return error.ToError(); + return llvm::Error::success(); } @@ -166,10 +179,10 @@ return err; } - m_trace.m_settings = *raw_settings.getAsObject(); - m_trace.m_settings_dir = m_settings_dir; + for (auto target_sp : m_targets) + target_sp->SetTrace(m_trace.shared_from_this()); + m_trace.m_thread_to_trace_file_map = m_thread_to_trace_file_map; - m_trace.m_targets = m_targets; return llvm::Error::success(); } diff --git a/lldb/test/API/commands/trace/TestTraceDumpInstructions.py b/lldb/test/API/commands/trace/TestTraceDumpInstructions.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/commands/trace/TestTraceDumpInstructions.py @@ -0,0 +1,49 @@ +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil +from lldbsuite.test.decorators import * + +class TestTraceDumpInstructions(TestBase): + + mydir = TestBase.compute_mydir(__file__) + NO_DEBUG_INFO_TESTCASE = True + + def setUp(self): + TestBase.setUp(self) + if 'intel-pt' not in configuration.enabled_plugins: + self.skipTest("The intel-pt test plugin is not enabled") + + def testErrorMessages(self): + # We first check the output when there are no targets + self.expect("thread trace dump instructions", + substrs=["error: invalid target, create a target using the 'target create' command"], + error=True) + + # We now check the output when there's a non-running target + self.expect("target create " + os.path.join(self.getSourceDir(), "intelpt-trace", "a.out")) + + self.expect("thread trace dump instructions", + substrs=["error: invalid process"], + error=True) + + # Now we check the output when there's a running target without a trace + self.expect("b main") + self.expect("r") + + self.expect("thread trace dump instructions", + substrs=["error: no trace in this target"]) + + def testDumpInstructions(self): + self.expect("trace load -v " + os.path.join(self.getSourceDir(), "intelpt-trace", "trace.json"), + substrs=["intel-pt"]) + + self.expect("thread trace dump instructions", + substrs=['placeholder trace of tid 3842849, count = 10, offset = 0']) + + # We check if we can pass count and offset + self.expect("thread trace dump instructions -c 5 -o 10", + substrs=['placeholder trace of tid 3842849, count = 5, offset = 10']) + + # We check if we can access the thread by index id + self.expect("thread trace dump instructions 1", + substrs=['placeholder trace of tid 3842849, count = 10, offset = 0']) diff --git a/lldb/test/API/commands/trace/TestTraceLoad.py b/lldb/test/API/commands/trace/TestTraceLoad.py --- a/lldb/test/API/commands/trace/TestTraceLoad.py +++ b/lldb/test/API/commands/trace/TestTraceLoad.py @@ -35,6 +35,10 @@ self.assertEqual("6AA9A4E2-6F28-2F33-377D-59FECE874C71-5B41261A", module.GetUUIDString()) + # check that the Process and Thread objects were created correctly + self.expect("thread info", substrs=["tid = 3842849"]) + self.expect("thread list", substrs=["Process 1234 stopped", "tid = 3842849"]) + def testLoadInvalidTraces(self): src_dir = self.getSourceDir()