https://github.com/DavidSpickett created https://github.com/llvm/llvm-project/pull/123945
This reverts commit 22561cfb443267905d4190f0e2a738e6b412457f and fixes b7b9ccf44988edf49886743ae5c3cf4184db211f (#112079). The problem is that x86_64 and Arm 32-bit have memory regions above the stack that are readable but not writeable. First Arm: ``` (lldb) memory region --all <...> [0x00000000fffcf000-0x00000000ffff0000) rw- [stack] [0x00000000ffff0000-0x00000000ffff1000) r-x [vectors] [0x00000000ffff1000-0xffffffffffffffff) --- ``` Then x86_64: ``` $ cat /proc/self/maps <...> 7ffdcd148000-7ffdcd16a000 rw-p 00000000 00:00 0 [stack] 7ffdcd193000-7ffdcd196000 r--p 00000000 00:00 0 [vvar] 7ffdcd196000-7ffdcd197000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] ``` Compare this to AArch64 where the test did pass: ``` $ cat /proc/self/maps <...> ffffb87dc000-ffffb87dd000 r--p 00000000 00:00 0 [vvar] ffffb87dd000-ffffb87de000 r-xp 00000000 00:00 0 [vdso] ffffb87de000-ffffb87e0000 r--p 0002a000 00:3c 76927217 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 ffffb87e0000-ffffb87e2000 rw-p 0002c000 00:3c 76927217 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 fffff4216000-fffff4237000 rw-p 00000000 00:00 0 [stack] ``` To solve this, look up the memory region of the stack pointer (using https://lldb.llvm.org/resources/lldbgdbremote.html#qmemoryregioninfo-addr) and constrain the read to within that region. Since we know the stack is all readable and writeable. >From ecb1b90e109df650ef1b50cc3d07b56fd302e274 Mon Sep 17 00:00:00 2001 From: David Spickett <david.spick...@linaro.org> Date: Wed, 22 Jan 2025 10:52:16 +0000 Subject: [PATCH] Reland "[lldb] Implement basic support for reverse-continue" (#123906)" This reverts commit 22561cfb443267905d4190f0e2a738e6b412457f and fixes b7b9ccf44988edf49886743ae5c3cf4184db211f (#112079). The problem is that x86_64 and Arm 32-bit have memory regions above the stack that are readable but not writeable. First Arm: ``` (lldb) memory region --all <...> [0x00000000fffcf000-0x00000000ffff0000) rw- [stack] [0x00000000ffff0000-0x00000000ffff1000) r-x [vectors] [0x00000000ffff1000-0xffffffffffffffff) --- ``` Then x86_64: ``` $ cat /proc/self/maps <...> 7ffdcd148000-7ffdcd16a000 rw-p 00000000 00:00 0 [stack] 7ffdcd193000-7ffdcd196000 r--p 00000000 00:00 0 [vvar] 7ffdcd196000-7ffdcd197000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] ``` Compare this to AArch64 where the test did pass: ``` $ cat /proc/self/maps <...> ffffb87dc000-ffffb87dd000 r--p 00000000 00:00 0 [vvar] ffffb87dd000-ffffb87de000 r-xp 00000000 00:00 0 [vdso] ffffb87de000-ffffb87e0000 r--p 0002a000 00:3c 76927217 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 ffffb87e0000-ffffb87e2000 rw-p 0002c000 00:3c 76927217 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 fffff4216000-fffff4237000 rw-p 00000000 00:00 0 [stack] ``` To solve this, look up the memory region of the stack pointer and constrain the read to within that region. Since we know the stack is all readable and writeable. --- lldb/include/lldb/API/SBProcess.h | 1 + lldb/include/lldb/Target/Process.h | 28 +- lldb/include/lldb/Target/StopInfo.h | 7 + lldb/include/lldb/Target/Thread.h | 9 +- lldb/include/lldb/Target/ThreadList.h | 6 +- lldb/include/lldb/Target/ThreadPlan.h | 13 + lldb/include/lldb/Target/ThreadPlanBase.h | 2 + lldb/include/lldb/lldb-enumerations.h | 6 + .../Python/lldbsuite/test/gdbclientutils.py | 5 +- .../Python/lldbsuite/test/lldbgdbproxy.py | 175 ++++++ .../Python/lldbsuite/test/lldbreverse.py | 528 ++++++++++++++++++ .../Python/lldbsuite/test/lldbtest.py | 2 + .../tools/lldb-server/lldbgdbserverutils.py | 14 +- lldb/source/API/SBProcess.cpp | 12 + lldb/source/API/SBThread.cpp | 2 + .../source/Interpreter/CommandInterpreter.cpp | 3 +- .../Process/Linux/NativeThreadLinux.cpp | 3 + .../Process/MacOSX-Kernel/ProcessKDP.cpp | 8 +- .../Process/MacOSX-Kernel/ProcessKDP.h | 2 +- .../Process/Windows/Common/ProcessWindows.cpp | 9 +- .../Process/Windows/Common/ProcessWindows.h | 2 +- .../GDBRemoteCommunicationClient.cpp | 20 + .../gdb-remote/GDBRemoteCommunicationClient.h | 6 + .../GDBRemoteCommunicationServerLLGS.cpp | 1 + .../Process/gdb-remote/ProcessGDBRemote.cpp | 98 +++- .../Process/gdb-remote/ProcessGDBRemote.h | 4 +- .../Process/scripted/ScriptedProcess.cpp | 9 +- .../Process/scripted/ScriptedProcess.h | 2 +- lldb/source/Target/Process.cpp | 24 +- lldb/source/Target/StopInfo.cpp | 28 + lldb/source/Target/Thread.cpp | 9 +- lldb/source/Target/ThreadList.cpp | 32 +- lldb/source/Target/ThreadPlanBase.cpp | 4 + .../reverse-execution/Makefile | 3 + .../TestReverseContinueBreakpoints.py | 149 +++++ .../TestReverseContinueNotSupported.py | 31 + .../TestReverseContinueWatchpoints.py | 130 +++++ .../functionalities/reverse-execution/main.c | 25 + lldb/tools/lldb-dap/JSONUtils.cpp | 3 + lldb/tools/lldb-dap/LLDBUtils.cpp | 1 + 40 files changed, 1369 insertions(+), 47 deletions(-) create mode 100644 lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py create mode 100644 lldb/packages/Python/lldbsuite/test/lldbreverse.py create mode 100644 lldb/test/API/functionalities/reverse-execution/Makefile create mode 100644 lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py create mode 100644 lldb/test/API/functionalities/reverse-execution/TestReverseContinueNotSupported.py create mode 100644 lldb/test/API/functionalities/reverse-execution/TestReverseContinueWatchpoints.py create mode 100644 lldb/test/API/functionalities/reverse-execution/main.c diff --git a/lldb/include/lldb/API/SBProcess.h b/lldb/include/lldb/API/SBProcess.h index 1624e02070b1b2..882b8bd837131d 100644 --- a/lldb/include/lldb/API/SBProcess.h +++ b/lldb/include/lldb/API/SBProcess.h @@ -159,6 +159,7 @@ class LLDB_API SBProcess { lldb::SBError Destroy(); lldb::SBError Continue(); + lldb::SBError ContinueInDirection(lldb::RunDirection direction); lldb::SBError Stop(); diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h index a184e6dd891aff..b14eb3fbd91d00 100644 --- a/lldb/include/lldb/Target/Process.h +++ b/lldb/include/lldb/Target/Process.h @@ -1089,6 +1089,13 @@ class Process : public std::enable_shared_from_this<Process>, /// Returns an error object. virtual Status WillResume() { return Status(); } + /// Reports whether this process supports reverse execution. + /// + /// \return + /// Returns true if the process supports reverse execution (at least + /// under some circumstances). + virtual bool SupportsReverseDirection() { return false; } + /// Resumes all of a process's threads as configured using the Thread run /// control functions. /// @@ -1104,9 +1111,13 @@ class Process : public std::enable_shared_from_this<Process>, /// \see Thread:Resume() /// \see Thread:Step() /// \see Thread:Suspend() - virtual Status DoResume() { + virtual Status DoResume(lldb::RunDirection direction) { + if (direction == lldb::RunDirection::eRunForward) + return Status::FromErrorStringWithFormatv( + "error: {0} does not support resuming processes", GetPluginName()); return Status::FromErrorStringWithFormatv( - "error: {0} does not support resuming processes", GetPluginName()); + "error: {0} does not support reverse execution of processes", + GetPluginName()); } /// Called after resuming a process. @@ -2676,6 +2687,18 @@ void PruneThreadPlans(); const AddressRange &range, size_t alignment, Status &error); + /// Get the base run direction for the process. + /// The base direction is the direction the process will execute in + /// (forward or backward) if no thread plan overrides the direction. + lldb::RunDirection GetBaseDirection() const { return m_base_direction; } + /// Set the base run direction for the process. + /// As a side-effect, if this changes the base direction, then we + /// discard all non-base thread plans to ensure that when execution resumes + /// we definitely execute in the requested direction. + /// FIXME: this is overkill. In some situations ensuring the latter + /// would not require discarding all non-base thread plans. + void SetBaseDirection(lldb::RunDirection direction); + protected: friend class Trace; @@ -3075,6 +3098,7 @@ void PruneThreadPlans(); ThreadList m_extended_thread_list; ///< Constituent for extended threads that may be /// generated, cleared on natural stops + lldb::RunDirection m_base_direction; ///< ThreadPlanBase run direction uint32_t m_extended_thread_stop_id; ///< The natural stop id when ///extended_thread_list was last updated QueueList diff --git a/lldb/include/lldb/Target/StopInfo.h b/lldb/include/lldb/Target/StopInfo.h index 45beac129e86f7..9a13371708be52 100644 --- a/lldb/include/lldb/Target/StopInfo.h +++ b/lldb/include/lldb/Target/StopInfo.h @@ -20,6 +20,7 @@ namespace lldb_private { class StopInfo : public std::enable_shared_from_this<StopInfo> { friend class Process::ProcessEventData; friend class ThreadPlanBase; + friend class ThreadPlanReverseContinue; public: // Constructors and Destructors @@ -154,6 +155,12 @@ class StopInfo : public std::enable_shared_from_this<StopInfo> { static lldb::StopInfoSP CreateStopReasonProcessorTrace(Thread &thread, const char *description); + // This creates a StopInfo indicating that execution stopped because + // it was replaying some recorded execution history, and execution reached + // the end of that recorded history. + static lldb::StopInfoSP + CreateStopReasonHistoryBoundary(Thread &thread, const char *description); + static lldb::StopInfoSP CreateStopReasonFork(Thread &thread, lldb::pid_t child_pid, lldb::tid_t child_tid); diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h index ef66fa11574db9..cd82ee7d756030 100644 --- a/lldb/include/lldb/Target/Thread.h +++ b/lldb/include/lldb/Target/Thread.h @@ -200,14 +200,13 @@ class Thread : public std::enable_shared_from_this<Thread>, /// The User resume state for this thread. lldb::StateType GetResumeState() const { return m_resume_state; } - /// This function is called on all the threads before "ShouldResume" and - /// "WillResume" in case a thread needs to change its state before the - /// ThreadList polls all the threads to figure out which ones actually will - /// get to run and how. + // This function is called to determine whether the thread needs to + // step over a breakpoint and if so, push a step-over-breakpoint thread + // plan. /// /// \return /// True if we pushed a ThreadPlanStepOverBreakpoint - bool SetupForResume(); + bool SetupToStepOverBreakpointIfNeeded(lldb::RunDirection direction); // Do not override this function, it is for thread plan logic only bool ShouldResume(lldb::StateType resume_state); diff --git a/lldb/include/lldb/Target/ThreadList.h b/lldb/include/lldb/Target/ThreadList.h index f931bb83a8ceaf..c796975de60153 100644 --- a/lldb/include/lldb/Target/ThreadList.h +++ b/lldb/include/lldb/Target/ThreadList.h @@ -115,6 +115,10 @@ class ThreadList : public ThreadCollection { /// If a thread can "resume" without having to resume the target, it /// will return false for WillResume, and then the process will not be /// restarted. + /// Sets *direction to the run direction of the thread(s) that will + /// be resumed. If threads that we want to run disagree about the + /// direction, we execute forwards and pop any of the thread plans + /// that requested reverse execution. /// /// \return /// \b true instructs the process to resume normally, @@ -122,7 +126,7 @@ class ThreadList : public ThreadCollection { /// the process will not actually run. The thread must then return /// the correct StopInfo when asked. /// - bool WillResume(); + bool WillResume(lldb::RunDirection &direction); void DidResume(); diff --git a/lldb/include/lldb/Target/ThreadPlan.h b/lldb/include/lldb/Target/ThreadPlan.h index d6da484f4fc137..a7bac8cc5ecf6c 100644 --- a/lldb/include/lldb/Target/ThreadPlan.h +++ b/lldb/include/lldb/Target/ThreadPlan.h @@ -283,6 +283,15 @@ namespace lldb_private { // report_run_vote argument to the constructor works like report_stop_vote, and // is a way for a plan to instruct a sub-plan on how to respond to // ShouldReportStop. +// +// Reverse execution: +// +// Every thread plan has an associated RunDirection (forward or backward). +// For ThreadPlanBase, this direction is the Process's base direction. +// Whenever we resume the target, we need to ensure that the topmost thread +// plans for each runnable thread all agree on their direction. This is +// ensured in ThreadList::WillResume(), which chooses a direction and then +// discards thread plans incompatible with that direction. class ThreadPlan : public std::enable_shared_from_this<ThreadPlan>, public UserID { @@ -497,6 +506,10 @@ class ThreadPlan : public std::enable_shared_from_this<ThreadPlan>, virtual lldb::StateType GetPlanRunState() = 0; + virtual lldb::RunDirection GetDirection() const { + return lldb::RunDirection::eRunForward; + } + protected: // Constructors and Destructors ThreadPlan(ThreadPlanKind kind, const char *name, Thread &thread, diff --git a/lldb/include/lldb/Target/ThreadPlanBase.h b/lldb/include/lldb/Target/ThreadPlanBase.h index 5c44b9fb17b271..f4418d779a4dab 100644 --- a/lldb/include/lldb/Target/ThreadPlanBase.h +++ b/lldb/include/lldb/Target/ThreadPlanBase.h @@ -38,6 +38,8 @@ class ThreadPlanBase : public ThreadPlan { bool IsBasePlan() override { return true; } + lldb::RunDirection GetDirection() const override; + protected: bool DoWillResume(lldb::StateType resume_state, bool current_plan) override; bool DoPlanExplainsStop(Event *event_ptr) override; diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h index 50d2233509de6f..5f12e648684d7f 100644 --- a/lldb/include/lldb/lldb-enumerations.h +++ b/lldb/include/lldb/lldb-enumerations.h @@ -135,6 +135,9 @@ FLAGS_ENUM(LaunchFlags){ /// Thread Run Modes. enum RunMode { eOnlyThisThread, eAllThreads, eOnlyDuringStepping }; +/// Execution directions +enum RunDirection { eRunForward, eRunReverse }; + /// Byte ordering definitions. enum ByteOrder { eByteOrderInvalid = 0, @@ -254,6 +257,9 @@ enum StopReason { eStopReasonVFork, eStopReasonVForkDone, eStopReasonInterrupt, ///< Thread requested interrupt + // Indicates that execution stopped because the debugger backend relies + // on recorded data and we reached the end of that data. + eStopReasonHistoryBoundary, }; /// Command Return Status Types. diff --git a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py index 1784487323ad6b..732d6171320680 100644 --- a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py +++ b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py @@ -510,8 +510,9 @@ def start(self): self._thread.start() def stop(self): - self._thread.join() - self._thread = None + if self._thread is not None: + self._thread.join() + self._thread = None def get_connect_address(self): return self._socket.get_connect_address() diff --git a/lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py b/lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py new file mode 100644 index 00000000000000..a84c80f155a0a4 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py @@ -0,0 +1,175 @@ +import logging +import os +import os.path +import random + +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test.gdbclientutils import * +import lldbgdbserverutils +from lldbsuite.support import seven + + +class GDBProxyTestBase(TestBase): + """ + Base class for gdbserver proxy tests. + + This class will setup and start a mock GDB server for the test to use. + It pases through requests to a regular lldb-server/debugserver and + forwards replies back to the LLDB under test. + """ + + """The gdbserver that we implement.""" + server = None + """The inner lldb-server/debugserver process that we proxy requests into.""" + monitor_server = None + monitor_sock = None + + server_socket_class = TCPServerSocket + + DEFAULT_TIMEOUT = 20 * (10 if ("ASAN_OPTIONS" in os.environ) else 1) + + _verbose_log_handler = None + _log_formatter = logging.Formatter(fmt="%(asctime)-15s %(levelname)-8s %(message)s") + + def setUpBaseLogging(self): + self.logger = logging.getLogger(__name__) + + self.logger.propagate = False + self.logger.setLevel(logging.DEBUG) + + # log all warnings to stderr + handler = logging.StreamHandler() + handler.setLevel(logging.WARNING) + handler.setFormatter(self._log_formatter) + self.logger.addHandler(handler) + + def setUp(self): + TestBase.setUp(self) + + self.setUpBaseLogging() + + if self.isVerboseLoggingRequested(): + # If requested, full logs go to a log file + log_file_name = self.getLogBasenameForCurrentTest() + "-proxy.log" + self._verbose_log_handler = logging.FileHandler(log_file_name) + self._verbose_log_handler.setFormatter(self._log_formatter) + self._verbose_log_handler.setLevel(logging.DEBUG) + self.logger.addHandler(self._verbose_log_handler) + + if lldbplatformutil.getPlatform() == "macosx": + self.debug_monitor_exe = lldbgdbserverutils.get_debugserver_exe() + self.debug_monitor_extra_args = [] + else: + self.debug_monitor_exe = lldbgdbserverutils.get_lldb_server_exe() + self.debug_monitor_extra_args = ["gdbserver"] + self.assertIsNotNone(self.debug_monitor_exe) + + self.server = MockGDBServer(self.server_socket_class()) + self.server.responder = self + + def tearDown(self): + # TestBase.tearDown will kill the process, but we need to kill it early + # so its client connection closes and we can stop the server before + # finally calling the base tearDown. + if self.process() is not None: + self.process().Kill() + self.server.stop() + + self.logger.removeHandler(self._verbose_log_handler) + self._verbose_log_handler = None + + TestBase.tearDown(self) + + def isVerboseLoggingRequested(self): + # We will report our detailed logs if the user requested that the "gdb-remote" channel is + # logged. + return any(("gdb-remote" in channel) for channel in lldbtest_config.channels) + + def connect(self, target): + """ + Create a process by connecting to the mock GDB server. + """ + self.prep_debug_monitor_and_inferior() + self.server.start() + + listener = self.dbg.GetListener() + error = lldb.SBError() + process = target.ConnectRemote( + listener, self.server.get_connect_url(), "gdb-remote", error + ) + self.assertTrue(error.Success(), error.description) + self.assertTrue(process, PROCESS_IS_VALID) + return process + + def prep_debug_monitor_and_inferior(self): + inferior_exe_path = self.getBuildArtifact("a.out") + self.connect_to_debug_monitor([inferior_exe_path]) + self.assertIsNotNone(self.monitor_server) + self.initial_handshake() + + def initial_handshake(self): + self.monitor_server.send_packet(seven.bitcast_to_bytes("+")) + reply = seven.bitcast_to_string(self.monitor_server.get_normal_packet()) + self.assertEqual(reply, "+") + self.monitor_server.send_packet(seven.bitcast_to_bytes("QStartNoAckMode")) + reply = seven.bitcast_to_string(self.monitor_server.get_normal_packet()) + self.assertEqual(reply, "+") + reply = seven.bitcast_to_string(self.monitor_server.get_normal_packet()) + self.assertEqual(reply, "OK") + self.monitor_server.set_validate_checksums(False) + self.monitor_server.send_packet(seven.bitcast_to_bytes("+")) + reply = seven.bitcast_to_string(self.monitor_server.get_normal_packet()) + self.assertEqual(reply, "+") + + def get_debug_monitor_command_line_args(self, connect_address, launch_args): + return ( + self.debug_monitor_extra_args + + ["--reverse-connect", connect_address] + + launch_args + ) + + def launch_debug_monitor(self, launch_args): + family, type, proto, _, addr = socket.getaddrinfo( + "localhost", 0, proto=socket.IPPROTO_TCP + )[0] + sock = socket.socket(family, type, proto) + sock.settimeout(self.DEFAULT_TIMEOUT) + sock.bind(addr) + sock.listen(1) + addr = sock.getsockname() + connect_address = "[{}]:{}".format(*addr) + + commandline_args = self.get_debug_monitor_command_line_args( + connect_address, launch_args + ) + + # Start the server. + self.logger.info(f"Spawning monitor {commandline_args}") + monitor_process = self.spawnSubprocess( + self.debug_monitor_exe, commandline_args, install_remote=False + ) + self.assertIsNotNone(monitor_process) + + self.monitor_sock = sock.accept()[0] + self.monitor_sock.settimeout(self.DEFAULT_TIMEOUT) + return monitor_process + + def connect_to_debug_monitor(self, launch_args): + monitor_process = self.launch_debug_monitor(launch_args) + # Turn off checksum validation because debugserver does not produce + # correct checksums. + self.monitor_server = lldbgdbserverutils.Server( + self.monitor_sock, monitor_process + ) + + def respond(self, packet): + """Subclasses can override this to change how packets are handled.""" + return self.pass_through(packet) + + def pass_through(self, packet): + self.logger.info(f"Sending packet {packet}") + self.monitor_server.send_packet(seven.bitcast_to_bytes(packet)) + reply = seven.bitcast_to_string(self.monitor_server.get_normal_packet()) + self.logger.info(f"Received reply {reply}") + return reply diff --git a/lldb/packages/Python/lldbsuite/test/lldbreverse.py b/lldb/packages/Python/lldbsuite/test/lldbreverse.py new file mode 100644 index 00000000000000..18a8ee062015cc --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/lldbreverse.py @@ -0,0 +1,528 @@ +import os +import os.path +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test.gdbclientutils import * +from lldbsuite.test.lldbgdbproxy import * +import lldbgdbserverutils +import re + + +class ThreadSnapshot: + def __init__(self, thread_id, registers): + self.thread_id = thread_id + self.registers = registers + + +class MemoryBlockSnapshot: + def __init__(self, address, data): + self.address = address + self.data = data + + +class StateSnapshot: + def __init__(self, thread_snapshots, memory): + self.thread_snapshots = thread_snapshots + self.memory = memory + self.thread_id = None + + +class RegisterInfo: + def __init__(self, lldb_index, bitsize, little_endian): + self.lldb_index = lldb_index + self.bitsize = bitsize + self.little_endian = little_endian + + +BELOW_STACK_POINTER = 16384 +ABOVE_STACK_POINTER = 4096 + +BLOCK_SIZE = 1024 + +SOFTWARE_BREAKPOINTS = 0 +HARDWARE_BREAKPOINTS = 1 +WRITE_WATCHPOINTS = 2 + + +class ReverseTestBase(GDBProxyTestBase): + """ + Base class for tests that need reverse execution. + + This class uses a gdbserver proxy to add very limited reverse- + execution capability to lldb-server/debugserver for testing + purposes only. + + To use this class, run the inferior forward until some stopping point. + Then call `start_recording()` and execute forward again until reaching + a software breakpoint; this class records the state before each execution executes. + At that point, the server will accept "bc" and "bs" packets to step + backwards through the state. + When executing during recording, we only allow single-step and continue without + delivering a signal, and only software breakpoint stops are allowed. + + We assume that while recording is enabled, the only effects of instructions + are on general-purpose registers (read/written by the 'g' and 'G' packets) + and on memory bytes between [SP - BELOW_STACK_POINTER, SP + ABOVE_STACK_POINTER). + """ + + NO_DEBUG_INFO_TESTCASE = True + + """ + A list of StateSnapshots in time order. + + There is one snapshot per single-stepped instruction, + representing the state before that instruction was + executed. The last snapshot in the list is the + snapshot before the last instruction was executed. + This is an undo log; we snapshot a superset of the state that may have + been changed by the instruction's execution. + """ + snapshots = None + recording_enabled = False + + breakpoints = None + + pc_register_info = None + sp_register_info = None + general_purpose_register_info = None + + def __init__(self, *args, **kwargs): + GDBProxyTestBase.__init__(self, *args, **kwargs) + self.breakpoints = [set(), set(), set(), set(), set()] + + def respond(self, packet): + if not packet: + raise ValueError("Invalid empty packet") + if packet == self.server.PACKET_INTERRUPT: + # Don't send a response. We'll just run to completion. + return [] + if self.is_command(packet, "qSupported", ":"): + # Disable multiprocess support in the server and in LLDB + # since Mac debugserver doesn't support it and we want lldb-server to + # be consistent with that + reply = self.pass_through(packet.replace(";multiprocess", "")) + return reply.replace(";multiprocess", "") + ";ReverseStep+;ReverseContinue+" + if packet == "c" or packet == "s": + packet = "vCont;" + packet + elif ( + packet[0] == "c" or packet[0] == "s" or packet[0] == "C" or packet[0] == "S" + ): + raise ValueError( + "Old-style continuation packets with address or signal not supported yet" + ) + if self.is_command(packet, "vCont", ";"): + if self.recording_enabled: + return self.continue_with_recording(packet) + snapshots = [] + if packet == "bc": + return self.reverse_continue() + if packet == "bs": + return self.reverse_step() + if packet == "jThreadsInfo": + # Suppress this because it contains thread stop reasons which we might + # need to modify, and we don't want to have to implement that. + return "" + if packet[0] == "z" or packet[0] == "Z": + reply = self.pass_through(packet) + if reply == "OK": + self.update_breakpoints(packet) + return reply + return GDBProxyTestBase.respond(self, packet) + + def start_recording(self): + self.recording_enabled = True + self.snapshots = [] + + def stop_recording(self): + """ + Don't record when executing foward. + + Reverse execution is still supported until the next forward continue. + """ + self.recording_enabled = False + + def is_command(self, packet, cmd, follow_token): + return packet == cmd or packet[0 : len(cmd) + 1] == cmd + follow_token + + def update_breakpoints(self, packet): + m = re.match("([zZ])([01234]),([0-9a-f]+),([0-9a-f]+)", packet) + if m is None: + raise ValueError("Invalid breakpoint packet: " + packet) + t = int(m.group(2)) + addr = int(m.group(3), 16) + kind = int(m.group(4), 16) + if m.group(1) == "Z": + self.breakpoints[t].add((addr, kind)) + else: + self.breakpoints[t].discard((addr, kind)) + + def breakpoint_triggered_at(self, pc): + if any(addr == pc for addr, kind in self.breakpoints[SOFTWARE_BREAKPOINTS]): + return True + if any(addr == pc for addr, kind in self.breakpoints[HARDWARE_BREAKPOINTS]): + return True + return False + + def watchpoint_triggered(self, new_value_block, current_contents): + """Returns the address or None.""" + for watch_addr, kind in self.breakpoints[WRITE_WATCHPOINTS]: + for offset in range(0, kind): + addr = watch_addr + offset + if ( + addr >= new_value_block.address + and addr < new_value_block.address + len(new_value_block.data) + ): + index = addr - new_value_block.address + if ( + new_value_block.data[index * 2 : (index + 1) * 2] + != current_contents[index * 2 : (index + 1) * 2] + ): + return watch_addr + return None + + def continue_with_recording(self, packet): + self.logger.debug("Continue with recording enabled") + + step_packet = "vCont;s" + if packet == "vCont": + requested_step = False + else: + m = re.match("vCont;(c|s)(.*)", packet) + if m is None: + raise ValueError("Unsupported vCont packet: " + packet) + requested_step = m.group(1) == "s" + step_packet += m.group(2) + + while True: + snapshot = self.capture_snapshot() + reply = self.pass_through(step_packet) + (stop_signal, stop_pairs) = self.parse_stop_reply(reply) + if stop_signal != 5: + raise ValueError("Unexpected stop signal: " + reply) + is_swbreak = False + thread_id = None + for key, value in stop_pairs.items(): + if key == "thread": + thread_id = self.parse_thread_id(value) + continue + if re.match("[0-9a-f]+", key): + continue + if key == "swbreak" or (key == "reason" and value == "breakpoint"): + is_swbreak = True + continue + if key == "metype": + reason = self.stop_reason_from_mach_exception(stop_pairs) + if reason == "breakpoint": + is_swbreak = True + elif reason != "singlestep": + raise ValueError(f"Unsupported stop reason in {reply}") + continue + if key in [ + "name", + "threads", + "thread-pcs", + "reason", + "mecount", + "medata", + "memory", + ]: + continue + raise ValueError(f"Unknown stop key '{key}' in {reply}") + if is_swbreak: + self.logger.debug("Recording stopped") + return reply + if thread_id is None: + return ValueError("Expected thread ID: " + reply) + snapshot.thread_id = thread_id + self.snapshots.append(snapshot) + if requested_step: + self.logger.debug("Recording stopped for step") + return reply + + def stop_reason_from_mach_exception(self, stop_pairs): + # See StopInfoMachException::CreateStopReasonWithMachException. + if int(stop_pairs["metype"]) != 6: # EXC_BREAKPOINT + raise ValueError(f"Unsupported exception type {value} in {reply}") + medata = stop_pairs["medata"] + arch = self.getArchitecture() + if arch in ["amd64", "i386", "x86_64"]: + if int(medata[0], 16) == 2: + return "breakpoint" + if int(medata[0], 16) == 1 and int(medata[1], 16) == 0: + return "singlestep" + elif arch in ["arm64", "arm64e"]: + if int(medata[0], 16) == 1 and int(medata[1], 16) != 0: + return "breakpoint" + elif int(medata[0], 16) == 1 and int(medata[1], 16) == 0: + return "singlestep" + else: + raise ValueError(f"Unsupported architecture '{arch}'") + raise ValueError(f"Unsupported exception details in {reply}") + + def parse_stop_reply(self, reply): + if not reply: + raise ValueError("Invalid empty packet") + if reply[0] == "T" and len(reply) >= 3: + result = {} + for k, v in self.parse_pairs(reply[3:]): + if k in ["medata", "memory"]: + if k in result: + result[k].append(v) + else: + result[k] = [v] + else: + result[k] = v + return (int(reply[1:3], 16), result) + raise ValueError("Unsupported stop reply: " + reply) + + def parse_pairs(self, text): + for pair in text.split(";"): + if not pair: + continue + m = re.match("([^:]+):(.*)", pair) + if m is None: + raise ValueError("Invalid pair text: " + text) + yield (m.group(1), m.group(2)) + + def capture_snapshot(self): + """Snapshot all threads and their stack memories.""" + self.ensure_register_info() + current_thread = self.get_current_thread() + thread_snapshots = [] + memory = [] + for thread_id in self.get_thread_list(): + registers = {} + for index in sorted(self.general_purpose_register_info.keys()): + reply = self.pass_through(f"p{index:x};thread:{thread_id:x};") + if reply == "" or reply[0] == "E": + raise ValueError("Can't read register") + registers[index] = reply + thread_snapshot = ThreadSnapshot(thread_id, registers) + thread_sp = self.get_register( + self.sp_register_info, thread_snapshot.registers + ) + + # The memory above or below the stack pointer may be mapped, but not + # both readable and writeable. For example on Arm 32-bit Linux, there + # is a "[vectors]" mapping above the stack, which can be read but not + # written to. + # + # Therefore, we should limit any reads to the stack region, which we + # know is readable and writeable. + region_info = self.get_memory_region_info(thread_sp) + lower = max(thread_sp - BELOW_STACK_POINTER, region_info["start"]) + upper = min( + thread_sp + ABOVE_STACK_POINTER, + region_info["start"] + region_info["size"], + ) + + memory += self.read_memory(lower, upper) + thread_snapshots.append(thread_snapshot) + self.set_current_thread(current_thread) + return StateSnapshot(thread_snapshots, memory) + + def restore_snapshot(self, snapshot): + """ + Restore the snapshot during reverse execution. + + If this triggers a breakpoint or watchpoint, return the stop reply, + otherwise None. + """ + current_thread = self.get_current_thread() + stop_reasons = [] + for thread_snapshot in snapshot.thread_snapshots: + thread_id = thread_snapshot.thread_id + for lldb_index in sorted(thread_snapshot.registers.keys()): + data = thread_snapshot.registers[lldb_index] + reply = self.pass_through( + f"P{lldb_index:x}={data};thread:{thread_id:x};" + ) + if reply != "OK": + raise ValueError("Can't restore thread register") + if thread_id == snapshot.thread_id: + new_pc = self.get_register( + self.pc_register_info, thread_snapshot.registers + ) + if self.breakpoint_triggered_at(new_pc): + stop_reasons.append([("reason", "breakpoint")]) + self.set_current_thread(current_thread) + for block in snapshot.memory: + current_memory = self.pass_through( + f"m{block.address:x},{(len(block.data)//2):x}" + ) + if not current_memory or current_memory[0] == "E": + raise ValueError("Can't read back memory") + reply = self.pass_through( + f"M{block.address:x},{len(block.data)//2:x}:" + block.data + ) + if reply != "OK": + raise ValueError("Can't restore memory") + watch_addr = self.watchpoint_triggered(block, current_memory) + if watch_addr is not None: + stop_reasons.append( + [("reason", "watchpoint"), ("watch", f"{watch_addr:x}")] + ) + if stop_reasons: + pairs = ";".join(f"{key}:{value}" for key, value in stop_reasons[0]) + return f"T05thread:{snapshot.thread_id:x};{pairs};" + return None + + def reverse_step(self): + if not self.snapshots: + self.logger.debug("Reverse-step at history boundary") + return self.history_boundary_reply(self.get_current_thread()) + self.logger.debug("Reverse-step started") + snapshot = self.snapshots.pop() + stop_reply = self.restore_snapshot(snapshot) + self.set_current_thread(snapshot.thread_id) + self.logger.debug("Reverse-step stopped") + if stop_reply is None: + return self.singlestep_stop_reply(snapshot.thread_id) + return stop_reply + + def reverse_continue(self): + self.logger.debug("Reverse-continue started") + thread_id = None + while self.snapshots: + snapshot = self.snapshots.pop() + stop_reply = self.restore_snapshot(snapshot) + thread_id = snapshot.thread_id + if stop_reply is not None: + self.set_current_thread(thread_id) + self.logger.debug("Reverse-continue stopped") + return stop_reply + if thread_id is None: + thread_id = self.get_current_thread() + else: + self.set_current_thread(snapshot.thread_id) + self.logger.debug("Reverse-continue stopped at history boundary") + return self.history_boundary_reply(thread_id) + + def get_current_thread(self): + reply = self.pass_through("qC") + return self.parse_thread_id(reply[2:]) + + def parse_thread_id(self, thread_id): + m = re.match("([0-9a-f]+)", thread_id) + if m is None: + raise ValueError("Invalid thread ID: " + thread_id) + return int(m.group(1), 16) + + def history_boundary_reply(self, thread_id): + return f"T00thread:{thread_id:x};replaylog:begin;" + + def singlestep_stop_reply(self, thread_id): + return f"T05thread:{thread_id:x};" + + def set_current_thread(self, thread_id): + """ + Set current thread in inner gdbserver. + """ + if thread_id >= 0: + self.pass_through(f"Hg{thread_id:x}") + self.pass_through(f"Hc{thread_id:x}") + else: + self.pass_through(f"Hc-1") + self.pass_through(f"Hg-1") + + def get_register(self, register_info, registers): + if register_info.bitsize % 8 != 0: + raise ValueError("Register size must be a multiple of 8 bits") + if register_info.lldb_index not in registers: + raise ValueError("Register value not captured") + data = registers[register_info.lldb_index] + num_bytes = register_info.bitsize // 8 + bytes = [] + for i in range(0, num_bytes): + bytes.append(int(data[i * 2 : (i + 1) * 2], 16)) + if register_info.little_endian: + bytes.reverse() + result = 0 + for byte in bytes: + result = (result << 8) + byte + return result + + def get_memory_region_info(self, addr): + reply = self.pass_through(f"qMemoryRegionInfo:{addr:x}") + if not reply or reply[0] == "E": + raise RuntimeError("Failed to get memory region info.") + + # Valid reply looks like: + # start:fffcf000;size:21000;permissions:rw;flags:;name:5b737461636b5d; + values = [v for v in reply.strip().split(";") if v] + region_info = {} + for value in values: + key, value = value.split( + ":", + ) + region_info[key] = value + + if not ("start" in region_info and "size" in region_info): + raise RuntimeError("Did not get extent of memory region.") + + region_info["start"] = int(region_info["start"], 16) + region_info["size"] = int(region_info["size"], 16) + + return region_info + + def read_memory(self, start_addr, end_addr): + """ + Read a region of memory from the target. + + Some of the addresses may extend into memory we cannot read, skip those. + + Return a list of blocks containing the valid area(s) in the + requested range. + """ + regions = [] + start_addr = start_addr - (start_addr % BLOCK_SIZE) + if end_addr % BLOCK_SIZE > 0: + end_addr = end_addr - (end_addr % BLOCK_SIZE) + BLOCK_SIZE + for addr in range(start_addr, end_addr, BLOCK_SIZE): + reply = self.pass_through(f"m{addr:x},{(BLOCK_SIZE - 1):x}") + if reply and reply[0] != "E": + block = MemoryBlockSnapshot(addr, reply) + regions.append(block) + return regions + + def ensure_register_info(self): + if self.general_purpose_register_info is not None: + return + reply = self.pass_through("qHostInfo") + little_endian = any( + kv == ("endian", "little") for kv in self.parse_pairs(reply) + ) + self.general_purpose_register_info = {} + lldb_index = 0 + while True: + reply = self.pass_through(f"qRegisterInfo{lldb_index:x}") + if not reply or reply[0] == "E": + break + info = {k: v for k, v in self.parse_pairs(reply)} + reg_info = RegisterInfo(lldb_index, int(info["bitsize"]), little_endian) + if ( + info["set"] == "General Purpose Registers" + and not "container-regs" in info + ): + self.general_purpose_register_info[lldb_index] = reg_info + if "generic" in info: + if info["generic"] == "pc": + self.pc_register_info = reg_info + elif info["generic"] == "sp": + self.sp_register_info = reg_info + lldb_index += 1 + if self.pc_register_info is None or self.sp_register_info is None: + raise ValueError("Can't find generic pc or sp register") + + def get_thread_list(self): + threads = [] + reply = self.pass_through("qfThreadInfo") + while True: + if not reply: + raise ValueError("Missing reply packet") + if reply[0] == "m": + for id in reply[1:].split(","): + threads.append(self.parse_thread_id(id)) + elif reply[0] == "l": + return threads + reply = self.pass_through("qsThreadInfo") diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index 81b286340560dc..b0d2a27dba0eef 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -143,6 +143,8 @@ STOPPED_DUE_TO_WATCHPOINT = "Process should be stopped due to watchpoint" +STOPPED_DUE_TO_HISTORY_BOUNDARY = "Process should be stopped due to history boundary" + DATA_TYPES_DISPLAYED_CORRECTLY = "Data type(s) displayed correctly" VALID_BREAKPOINT = "Got a valid breakpoint" diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-server/lldbgdbserverutils.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-server/lldbgdbserverutils.py index 94376a16d39f6a..fc552ef887ce58 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-server/lldbgdbserverutils.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-server/lldbgdbserverutils.py @@ -863,6 +863,7 @@ def __init__(self, sock, proc=None): self._output_queue = [] self._sock = sock self._proc = proc + self._validate_checksums = True def send_raw(self, frame): self._sock.sendall(frame) @@ -873,6 +874,9 @@ def send_ack(self): def send_packet(self, packet): self.send_raw(b"$%s#%02x" % (packet, self._checksum(packet))) + def set_validate_checksums(self, validate): + self._validate_checksums = validate + @staticmethod def _checksum(packet): checksum = 0 @@ -931,12 +935,12 @@ def get_raw_output_packet(self): def get_raw_normal_packet(self): return self._read(self._normal_queue) - @staticmethod - def _get_payload(frame): + def _get_payload(self, frame): payload = frame[1:-3] - checksum = int(frame[-2:], 16) - if checksum != Server._checksum(payload): - raise ChecksumMismatch + if self._validate_checksums: + checksum = int(frame[-2:], 16) + if checksum != Server._checksum(payload): + raise ChecksumMismatch return payload def get_normal_packet(self): diff --git a/lldb/source/API/SBProcess.cpp b/lldb/source/API/SBProcess.cpp index 9773144723c34c..23ea449b30ccad 100644 --- a/lldb/source/API/SBProcess.cpp +++ b/lldb/source/API/SBProcess.cpp @@ -583,6 +583,18 @@ SBError SBProcess::Continue() { return sb_error; } +SBError SBProcess::ContinueInDirection(RunDirection direction) { + if (ProcessSP process_sp = GetSP()) { + if (direction == RunDirection::eRunReverse && + !process_sp->SupportsReverseDirection()) + return Status::FromErrorStringWithFormatv( + "error: {0} does not support reverse execution of processes", + GetPluginName()); + process_sp->SetBaseDirection(direction); + } + return Continue(); +} + SBError SBProcess::Destroy() { LLDB_INSTRUMENT_VA(this); diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp index cc848076dab5fa..d9469fc1390d62 100644 --- a/lldb/source/API/SBThread.cpp +++ b/lldb/source/API/SBThread.cpp @@ -172,6 +172,7 @@ size_t SBThread::GetStopReasonDataCount() { case eStopReasonInstrumentation: case eStopReasonProcessorTrace: case eStopReasonVForkDone: + case eStopReasonHistoryBoundary: // There is no data for these stop reasons. return 0; @@ -233,6 +234,7 @@ uint64_t SBThread::GetStopReasonDataAtIndex(uint32_t idx) { case eStopReasonInstrumentation: case eStopReasonProcessorTrace: case eStopReasonVForkDone: + case eStopReasonHistoryBoundary: // There is no data for these stop reasons. return 0; diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp index 764dcfd1903b19..284955a65a4429 100644 --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -2557,7 +2557,8 @@ bool CommandInterpreter::DidProcessStopAbnormally() const { const StopReason reason = stop_info->GetStopReason(); if (reason == eStopReasonException || reason == eStopReasonInstrumentation || - reason == eStopReasonProcessorTrace || reason == eStopReasonInterrupt) + reason == eStopReasonProcessorTrace || reason == eStopReasonInterrupt || + reason == eStopReasonHistoryBoundary) return true; if (reason == eStopReasonSignal) { diff --git a/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp b/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp index de047ee214c11e..b0aa664775b463 100644 --- a/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp +++ b/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp @@ -82,6 +82,9 @@ void LogThreadStopInfo(Log &log, const ThreadStopInfo &stop_info, case eStopReasonProcessorTrace: log.Printf("%s: %s processor trace", __FUNCTION__, header); return; + case eStopReasonHistoryBoundary: + log.Printf("%s: %s history boundary", __FUNCTION__, header); + return; default: log.Printf("%s: %s invalid stop reason %" PRIu32, __FUNCTION__, header, static_cast<uint32_t>(stop_info.reason)); diff --git a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp index 9b2907c6809965..ef57e7bfd1e425 100644 --- a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp +++ b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp @@ -402,9 +402,15 @@ lldb_private::DynamicLoader *ProcessKDP::GetDynamicLoader() { Status ProcessKDP::WillResume() { return Status(); } -Status ProcessKDP::DoResume() { +Status ProcessKDP::DoResume(RunDirection direction) { Status error; Log *log = GetLog(KDPLog::Process); + + if (direction == RunDirection::eRunReverse) + return Status::FromErrorStringWithFormatv( + "error: {0} does not support reverse execution of processes", + GetPluginName()); + // Only start the async thread if we try to do any process control if (!m_async_thread.IsJoinable()) StartAsyncThread(); diff --git a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.h b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.h index e5ec5914f9600d..1b71d83f70b087 100644 --- a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.h +++ b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.h @@ -90,7 +90,7 @@ class ProcessKDP : public lldb_private::Process { // Process Control lldb_private::Status WillResume() override; - lldb_private::Status DoResume() override; + lldb_private::Status DoResume(lldb::RunDirection direction) override; lldb_private::Status DoHalt(bool &caused_stop) override; diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp index 1bdacec221695e..7ff32ee96e0041 100644 --- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp +++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp @@ -236,11 +236,18 @@ ProcessWindows::DoAttachToProcessWithID(lldb::pid_t pid, return error; } -Status ProcessWindows::DoResume() { +Status ProcessWindows::DoResume(RunDirection direction) { Log *log = GetLog(WindowsLog::Process); llvm::sys::ScopedLock lock(m_mutex); Status error; + if (direction == RunDirection::eRunReverse) { + error.FromErrorStringWithFormatv( + "error: {0} does not support reverse execution of processes", + GetPluginName()); + return error; + } + StateType private_state = GetPrivateState(); if (private_state == eStateStopped || private_state == eStateCrashed) { LLDB_LOG(log, "process {0} is in state {1}. Resuming...", diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h index e97cfb790248be..97284b7cd1436e 100644 --- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h +++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h @@ -52,7 +52,7 @@ class ProcessWindows : public Process, public ProcessDebugger { Status DoAttachToProcessWithID( lldb::pid_t pid, const lldb_private::ProcessAttachInfo &attach_info) override; - Status DoResume() override; + Status DoResume(lldb::RunDirection direction) override; Status DoDestroy() override; Status DoHalt(bool &caused_stop) override; diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp index b3f1c6f052955b..adc311ce2dd280 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp @@ -199,6 +199,18 @@ uint64_t GDBRemoteCommunicationClient::GetRemoteMaxPacketSize() { return m_max_packet_size; } +bool GDBRemoteCommunicationClient::GetReverseContinueSupported() { + if (m_supports_reverse_continue == eLazyBoolCalculate) + GetRemoteQSupported(); + return m_supports_reverse_continue == eLazyBoolYes; +} + +bool GDBRemoteCommunicationClient::GetReverseStepSupported() { + if (m_supports_reverse_step == eLazyBoolCalculate) + GetRemoteQSupported(); + return m_supports_reverse_step == eLazyBoolYes; +} + bool GDBRemoteCommunicationClient::QueryNoAckModeSupported() { if (m_supports_not_sending_acks == eLazyBoolCalculate) { m_send_acks = true; @@ -295,6 +307,8 @@ void GDBRemoteCommunicationClient::ResetDiscoverableSettings(bool did_exec) { m_supports_qXfer_siginfo_read = eLazyBoolCalculate; m_supports_augmented_libraries_svr4_read = eLazyBoolCalculate; m_uses_native_signals = eLazyBoolCalculate; + m_supports_reverse_continue = eLazyBoolCalculate; + m_supports_reverse_step = eLazyBoolCalculate; m_supports_qProcessInfoPID = true; m_supports_qfProcessInfo = true; m_supports_qUserName = true; @@ -348,6 +362,8 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_memory_tagging = eLazyBoolNo; m_supports_qSaveCore = eLazyBoolNo; m_uses_native_signals = eLazyBoolNo; + m_supports_reverse_continue = eLazyBoolNo; + m_supports_reverse_step = eLazyBoolNo; m_max_packet_size = UINT64_MAX; // It's supposed to always be there, but if // not, we assume no limit @@ -401,6 +417,10 @@ void GDBRemoteCommunicationClient::GetRemoteQSupported() { m_supports_qSaveCore = eLazyBoolYes; else if (x == "native-signals+") m_uses_native_signals = eLazyBoolYes; + else if (x == "ReverseContinue+") + m_supports_reverse_continue = eLazyBoolYes; + else if (x == "ReverseStep+") + m_supports_reverse_step = eLazyBoolYes; // Look for a list of compressions in the features list e.g. // qXfer:features:read+;PacketSize=20000;qEcho+;SupportedCompressions=zlib- // deflate,lzma diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h index 898d176abc3465..116b47c1edf033 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h @@ -331,6 +331,10 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { bool GetMultiprocessSupported(); + bool GetReverseContinueSupported(); + + bool GetReverseStepSupported(); + LazyBool SupportsAllocDeallocMemory() // const { // Uncomment this to have lldb pretend the debug server doesn't respond to @@ -561,6 +565,8 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase { LazyBool m_supports_memory_tagging = eLazyBoolCalculate; LazyBool m_supports_qSaveCore = eLazyBoolCalculate; LazyBool m_uses_native_signals = eLazyBoolCalculate; + LazyBool m_supports_reverse_continue = eLazyBoolCalculate; + LazyBool m_supports_reverse_step = eLazyBoolCalculate; bool m_supports_qProcessInfoPID : 1, m_supports_qfProcessInfo : 1, m_supports_qUserName : 1, m_supports_qGroupName : 1, diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp index 8cdeaac5c7cb28..89d2730cfccd02 100644 --- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp @@ -716,6 +716,7 @@ static const char *GetStopReasonString(StopReason stop_reason) { return "vforkdone"; case eStopReasonInterrupt: return "async interrupt"; + case eStopReasonHistoryBoundary: case eStopReasonInstrumentation: case eStopReasonInvalid: case eStopReasonPlanComplete: diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp index 538c8680140091..fa511af9b6d549 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp @@ -169,6 +169,8 @@ class PluginProperties : public Properties { } }; +std::chrono::seconds ResumeTimeout() { return std::chrono::seconds(5); } + } // namespace static PluginProperties &GetGlobalPluginProperties() { @@ -1180,10 +1182,16 @@ Status ProcessGDBRemote::WillResume() { return Status(); } -Status ProcessGDBRemote::DoResume() { +bool ProcessGDBRemote::SupportsReverseDirection() { + return m_gdb_comm.GetReverseStepSupported() || + m_gdb_comm.GetReverseContinueSupported(); +} + +Status ProcessGDBRemote::DoResume(RunDirection direction) { Status error; Log *log = GetLog(GDBRLog::Process); - LLDB_LOGF(log, "ProcessGDBRemote::Resume()"); + LLDB_LOGF(log, "ProcessGDBRemote::Resume(%s)", + direction == RunDirection::eRunForward ? "" : "reverse"); ListenerSP listener_sp( Listener::MakeListener("gdb-remote.resume-packet-sent")); @@ -1197,12 +1205,24 @@ Status ProcessGDBRemote::DoResume() { StreamString continue_packet; bool continue_packet_error = false; - if (m_gdb_comm.HasAnyVContSupport()) { + // Number of threads continuing with "c", i.e. continuing without a signal + // to deliver. + const size_t num_continue_c_tids = m_continue_c_tids.size(); + // Number of threads continuing with "C", i.e. continuing with a signal to + // deliver. + const size_t num_continue_C_tids = m_continue_C_tids.size(); + // Number of threads continuing with "s", i.e. single-stepping. + const size_t num_continue_s_tids = m_continue_s_tids.size(); + // Number of threads continuing with "S", i.e. single-stepping with a signal + // to deliver. + const size_t num_continue_S_tids = m_continue_S_tids.size(); + if (direction == RunDirection::eRunForward && + m_gdb_comm.HasAnyVContSupport()) { std::string pid_prefix; if (m_gdb_comm.GetMultiprocessSupported()) pid_prefix = llvm::formatv("p{0:x-}.", GetID()); - if (m_continue_c_tids.size() == num_threads || + if (num_continue_c_tids == num_threads || (m_continue_c_tids.empty() && m_continue_C_tids.empty() && m_continue_s_tids.empty() && m_continue_S_tids.empty())) { // All threads are continuing @@ -1265,14 +1285,10 @@ Status ProcessGDBRemote::DoResume() { } else continue_packet_error = true; - if (continue_packet_error) { + if (direction == RunDirection::eRunForward && continue_packet_error) { // Either no vCont support, or we tried to use part of the vCont packet // that wasn't supported by the remote GDB server. We need to try and - // make a simple packet that can do our continue - const size_t num_continue_c_tids = m_continue_c_tids.size(); - const size_t num_continue_C_tids = m_continue_C_tids.size(); - const size_t num_continue_s_tids = m_continue_s_tids.size(); - const size_t num_continue_S_tids = m_continue_S_tids.size(); + // make a simple packet that can do our continue. if (num_continue_c_tids > 0) { if (num_continue_c_tids == num_threads) { // All threads are resuming... @@ -1363,9 +1379,59 @@ Status ProcessGDBRemote::DoResume() { } } + if (direction == RunDirection::eRunReverse) { + if (num_continue_s_tids > 0 || num_continue_S_tids > 0) { + if (!m_gdb_comm.GetReverseStepSupported()) { + LLDB_LOGF(log, "ProcessGDBRemote::DoResume: target does not " + "support reverse-stepping"); + return Status::FromErrorString( + "target does not support reverse-stepping"); + } + + if (num_continue_S_tids > 0) { + LLDB_LOGF( + log, + "ProcessGDBRemote::DoResume: Signals not supported in reverse"); + return Status::FromErrorString( + "can't deliver signals while running in reverse"); + } + + if (num_continue_s_tids > 1) { + LLDB_LOGF(log, "ProcessGDBRemote::DoResume: can't step multiple " + "threads in reverse"); + return Status::FromErrorString( + "can't step multiple threads while reverse-stepping"); + } + + m_gdb_comm.SetCurrentThreadForRun(m_continue_s_tids.front()); + continue_packet.PutCString("bs"); + } else { + if (!m_gdb_comm.GetReverseContinueSupported()) { + LLDB_LOGF(log, "ProcessGDBRemote::DoResume: target does not " + "support reverse-continue"); + return Status::FromErrorString( + "target does not support reverse-continue"); + } + + if (num_continue_C_tids > 0) { + LLDB_LOGF( + log, + "ProcessGDBRemote::DoResume: Signals not supported in reverse"); + return Status::FromErrorString( + "can't deliver signals while running in reverse"); + } + + // All threads continue whether requested or not --- + // we can't change how threads ran in the past. + continue_packet.PutCString("bc"); + } + + continue_packet_error = false; + } + if (continue_packet_error) { - error = - Status::FromErrorString("can't make continue packet for this resume"); + return Status::FromErrorString( + "can't make continue packet for this resume"); } else { EventSP event_sp; if (!m_async_thread.IsJoinable()) { @@ -1380,7 +1446,7 @@ Status ProcessGDBRemote::DoResume() { std::make_shared<EventDataBytes>(continue_packet.GetString()); m_async_broadcaster.BroadcastEvent(eBroadcastBitAsyncContinue, data_sp); - if (!listener_sp->GetEvent(event_sp, std::chrono::seconds(5))) { + if (!listener_sp->GetEvent(event_sp, ResumeTimeout())) { error = Status::FromErrorString("Resume timed out."); LLDB_LOGF(log, "ProcessGDBRemote::DoResume: Resume timed out."); } else if (event_sp->BroadcasterIs(&m_async_broadcaster)) { @@ -1863,6 +1929,10 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo( thread_sp->SetStopInfo(StopInfo::CreateStopReasonWithException( *thread_sp, description.c_str())); handled = true; + } else if (reason == "history boundary") { + thread_sp->SetStopInfo(StopInfo::CreateStopReasonHistoryBoundary( + *thread_sp, description.c_str())); + handled = true; } else if (reason == "exec") { did_exec = true; thread_sp->SetStopInfo( @@ -2318,6 +2388,8 @@ StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) { description = std::string(ostr.GetString()); } else if (key.compare("swbreak") == 0 || key.compare("hwbreak") == 0) { reason = "breakpoint"; + } else if (key.compare("replaylog") == 0) { + reason = "history boundary"; } else if (key.compare("library") == 0) { auto error = LoadModules(); if (error) { diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h index 2492795851388a..1cbd1e82b381d9 100644 --- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h +++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h @@ -111,7 +111,9 @@ class ProcessGDBRemote : public Process, // Process Control Status WillResume() override; - Status DoResume() override; + bool SupportsReverseDirection() override; + + Status DoResume(lldb::RunDirection direction) override; Status DoHalt(bool &caused_stop) override; diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp index d2111ce877ce55..3360bd9a044bd2 100644 --- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp @@ -182,10 +182,15 @@ void ScriptedProcess::DidResume() { m_pid = GetInterface().GetProcessID(); } -Status ScriptedProcess::DoResume() { +Status ScriptedProcess::DoResume(RunDirection direction) { LLDB_LOGF(GetLog(LLDBLog::Process), "ScriptedProcess::%s resuming process", __FUNCTION__); - return GetInterface().Resume(); + if (direction == RunDirection::eRunForward) + return GetInterface().Resume(); + // FIXME: Pipe reverse continue through Scripted Processes + return Status::FromErrorStringWithFormatv( + "error: {0} does not support reverse execution of processes", + GetPluginName()); } Status ScriptedProcess::DoAttach(const ProcessAttachInfo &attach_info) { diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h index 0335364b4010b2..8ebe4ca5f3d449 100644 --- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h +++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h @@ -52,7 +52,7 @@ class ScriptedProcess : public Process { void DidResume() override; - Status DoResume() override; + Status DoResume(lldb::RunDirection direction) override; Status DoAttachToProcessWithID(lldb::pid_t pid, const ProcessAttachInfo &attach_info) override; diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp index 89731f798deda8..c594fd94c9904b 100644 --- a/lldb/source/Target/Process.cpp +++ b/lldb/source/Target/Process.cpp @@ -437,7 +437,8 @@ Process::Process(lldb::TargetSP target_sp, ListenerSP listener_sp, m_mod_id(), m_process_unique_id(0), m_thread_index_id(0), m_thread_id_to_index_id_map(), m_exit_status(-1), m_thread_list_real(*this), m_thread_list(*this), m_thread_plans(*this), - m_extended_thread_list(*this), m_extended_thread_stop_id(0), + m_extended_thread_list(*this), + m_base_direction(RunDirection::eRunForward), m_extended_thread_stop_id(0), m_queue_list(this), m_queue_list_stop_id(0), m_unix_signals_sp(unix_signals_sp), m_abi_sp(), m_process_input_reader(), m_stdio_communication("process.stdio"), m_stdio_communication_mutex(), @@ -845,6 +846,7 @@ bool Process::HandleProcessStateChangedEvent( switch (thread_stop_reason) { case eStopReasonInvalid: case eStopReasonNone: + case eStopReasonHistoryBoundary: break; case eStopReasonSignal: { @@ -3235,6 +3237,13 @@ Status Process::ConnectRemote(llvm::StringRef remote_url) { return error; } +void Process::SetBaseDirection(RunDirection direction) { + if (m_base_direction == direction) + return; + m_thread_list.DiscardThreadPlans(); + m_base_direction = direction; +} + Status Process::PrivateResume() { Log *log(GetLog(LLDBLog::Process | LLDBLog::Step)); LLDB_LOGF(log, @@ -3261,18 +3270,25 @@ Status Process::PrivateResume() { // (suspended/running/stepping). Threads should also check their resume // signal in lldb::Thread::GetResumeSignal() to see if they are supposed to // start back up with a signal. - if (m_thread_list.WillResume()) { + RunDirection direction; + if (m_thread_list.WillResume(direction)) { + LLDB_LOGF(log, "Process::PrivateResume WillResume direction=%d", + direction); // Last thing, do the PreResumeActions. if (!RunPreResumeActions()) { error = Status::FromErrorString( "Process::PrivateResume PreResumeActions failed, not resuming."); + LLDB_LOGF( + log, + "Process::PrivateResume PreResumeActions failed, not resuming."); } else { m_mod_id.BumpResumeID(); - error = DoResume(); + error = DoResume(direction); if (error.Success()) { DidResume(); m_thread_list.DidResume(); - LLDB_LOGF(log, "Process thinks the process has resumed."); + LLDB_LOGF(log, + "Process::PrivateResume thinks the process has resumed."); } else { LLDB_LOGF(log, "Process::PrivateResume() DoResume failed."); return error; diff --git a/lldb/source/Target/StopInfo.cpp b/lldb/source/Target/StopInfo.cpp index 356917a45b7b34..355d3a9ad6e8f1 100644 --- a/lldb/source/Target/StopInfo.cpp +++ b/lldb/source/Target/StopInfo.cpp @@ -1269,6 +1269,29 @@ class StopInfoProcessorTrace : public StopInfo { } }; +// StopInfoHistoryBoundary + +class StopInfoHistoryBoundary : public StopInfo { +public: + StopInfoHistoryBoundary(Thread &thread, const char *description) + : StopInfo(thread, LLDB_INVALID_UID) { + if (description) + SetDescription(description); + } + + ~StopInfoHistoryBoundary() override = default; + + StopReason GetStopReason() const override { + return eStopReasonHistoryBoundary; + } + + const char *GetDescription() override { + if (m_description.empty()) + return "history boundary"; + return m_description.c_str(); + } +}; + // StopInfoThreadPlan class StopInfoThreadPlan : public StopInfo { @@ -1496,6 +1519,11 @@ StopInfoSP StopInfo::CreateStopReasonProcessorTrace(Thread &thread, return StopInfoSP(new StopInfoProcessorTrace(thread, description)); } +StopInfoSP StopInfo::CreateStopReasonHistoryBoundary(Thread &thread, + const char *description) { + return StopInfoSP(new StopInfoHistoryBoundary(thread, description)); +} + StopInfoSP StopInfo::CreateStopReasonWithExec(Thread &thread) { return StopInfoSP(new StopInfoExec(thread)); } diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp index b5261310970611..2c4d925c732227 100644 --- a/lldb/source/Target/Thread.cpp +++ b/lldb/source/Target/Thread.cpp @@ -617,7 +617,7 @@ void Thread::WillStop() { current_plan->WillStop(); } -bool Thread::SetupForResume() { +bool Thread::SetupToStepOverBreakpointIfNeeded(RunDirection direction) { if (GetResumeState() != eStateSuspended) { // First check whether this thread is going to "actually" resume at all. // For instance, if we're stepping from one level to the next of an @@ -632,10 +632,11 @@ bool Thread::SetupForResume() { // what the current plan is. lldb::RegisterContextSP reg_ctx_sp(GetRegisterContext()); - if (reg_ctx_sp) { + ProcessSP process_sp(GetProcess()); + if (reg_ctx_sp && process_sp && direction == eRunForward) { const addr_t thread_pc = reg_ctx_sp->GetPC(); BreakpointSiteSP bp_site_sp = - GetProcess()->GetBreakpointSiteList().FindByAddress(thread_pc); + process_sp->GetBreakpointSiteList().FindByAddress(thread_pc); if (bp_site_sp) { // Note, don't assume there's a ThreadPlanStepOverBreakpoint, the // target may not require anything special to step over a breakpoint. @@ -1742,6 +1743,8 @@ std::string Thread::StopReasonAsString(lldb::StopReason reason) { return "processor trace"; case eStopReasonInterrupt: return "async interrupt"; + case eStopReasonHistoryBoundary: + return "history boundary"; } return "StopReason = " + std::to_string(reason); diff --git a/lldb/source/Target/ThreadList.cpp b/lldb/source/Target/ThreadList.cpp index 6cbef330bf4888..99e2c1204146a5 100644 --- a/lldb/source/Target/ThreadList.cpp +++ b/lldb/source/Target/ThreadList.cpp @@ -508,7 +508,7 @@ void ThreadList::DiscardThreadPlans() { (*pos)->DiscardThreadPlans(true); } -bool ThreadList::WillResume() { +bool ThreadList::WillResume(RunDirection &direction) { // Run through the threads and perform their momentary actions. But we only // do this for threads that are running, user suspended threads stay where // they are. @@ -566,6 +566,12 @@ bool ThreadList::WillResume() { } } + if (thread_to_run != nullptr) { + direction = thread_to_run->GetCurrentPlan()->GetDirection(); + } else { + direction = m_process.GetBaseDirection(); + } + // Give all the threads that are likely to run a last chance to set up their // state before we negotiate who is actually going to get a chance to run... // Don't set to resume suspended threads, and if any thread wanted to stop @@ -577,7 +583,12 @@ bool ThreadList::WillResume() { // "StopOthers" plans which would then get to be part of the who-gets-to-run // negotiation, but they're coming in after the fact, and the threads that // are already set up should take priority. - thread_to_run->SetupForResume(); + if (thread_to_run->SetupToStepOverBreakpointIfNeeded(direction)) { + // We only need to step over breakpoints when running forward, and the + // step-over-breakpoint plan itself wants to run forward, so this + // keeps our desired direction. + assert(thread_to_run->GetCurrentPlan()->GetDirection() == direction); + } } else { for (pos = m_threads.begin(); pos != end; ++pos) { ThreadSP thread_sp(*pos); @@ -585,7 +596,11 @@ bool ThreadList::WillResume() { if (thread_sp->IsOperatingSystemPluginThread() && !thread_sp->GetBackingThread()) continue; - if (thread_sp->SetupForResume()) { + if (thread_sp->SetupToStepOverBreakpointIfNeeded(direction)) { + // We only need to step over breakpoints when running forward, and the + // step-over-breakpoint plan itself wants to run forward, so this + // keeps our desired direction. + assert(thread_sp->GetCurrentPlan()->GetDirection() == direction); // You can't say "stop others" and also want yourself to be suspended. assert(thread_sp->GetCurrentPlan()->RunState() != eStateSuspended); thread_to_run = thread_sp; @@ -626,6 +641,17 @@ bool ThreadList::WillResume() { if (!thread_sp->ShouldResume(run_state)) need_to_resume = false; } + if (need_to_resume) { + // Ensure all threads are running in the right direction + for (pos = m_threads.begin(); pos != end; ++pos) { + ThreadSP thread_sp(*pos); + while (thread_sp->GetCurrentPlan()->GetDirection() != direction) { + // This can't discard the base plan because its direction is + // m_process.GetBaseDirection() i.e. `direction`. + thread_sp->DiscardPlan(); + } + } + } } else { for (pos = m_threads.begin(); pos != end; ++pos) { ThreadSP thread_sp(*pos); diff --git a/lldb/source/Target/ThreadPlanBase.cpp b/lldb/source/Target/ThreadPlanBase.cpp index dfd2157e70d4ad..09437b0048c2cf 100644 --- a/lldb/source/Target/ThreadPlanBase.cpp +++ b/lldb/source/Target/ThreadPlanBase.cpp @@ -196,3 +196,7 @@ bool ThreadPlanBase::MischiefManaged() { // The base plan is never done. return false; } + +RunDirection ThreadPlanBase::GetDirection() const { + return m_process.GetBaseDirection(); +} diff --git a/lldb/test/API/functionalities/reverse-execution/Makefile b/lldb/test/API/functionalities/reverse-execution/Makefile new file mode 100644 index 00000000000000..10495940055b63 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py new file mode 100644 index 00000000000000..5b76e76f6fdb7b --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py @@ -0,0 +1,149 @@ +import lldb +import time +import unittest +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +from lldbsuite.test.gdbclientutils import * +from lldbsuite.test.lldbreverse import ReverseTestBase +from lldbsuite.test import lldbutil + + +class TestReverseContinueBreakpoints(ReverseTestBase): + def test_reverse_continue(self): + self.reverse_continue_internal(async_mode=False) + + def test_reverse_continue_async(self): + self.reverse_continue_internal(async_mode=True) + + def reverse_continue_internal(self, async_mode): + target, process, initial_threads = self.setup_recording(async_mode) + + # Reverse-continue. We'll stop at the point where we started recording. + status = process.ContinueInDirection(lldb.eRunReverse) + self.assertSuccess(status) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + self.expect( + "thread list", + STOPPED_DUE_TO_HISTORY_BOUNDARY, + substrs=["stopped", "stop reason = history boundary"], + ) + + # Continue forward normally until the target exits. + status = process.ContinueInDirection(lldb.eRunForward) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateExited] + ) + self.assertSuccess(status) + self.assertState(process.GetState(), lldb.eStateExited) + self.assertEqual(process.GetExitStatus(), 0) + + def test_reverse_continue_breakpoint(self): + self.reverse_continue_breakpoint_internal(async_mode=False) + + def test_reverse_continue_breakpoint_async(self): + self.reverse_continue_breakpoint_internal(async_mode=True) + + def reverse_continue_breakpoint_internal(self, async_mode): + target, process, initial_threads = self.setup_recording(async_mode) + + # Reverse-continue to the function "trigger_breakpoint". + trigger_bkpt = target.BreakpointCreateByName("trigger_breakpoint", None) + status = process.ContinueInDirection(lldb.eRunReverse) + self.assertSuccess(status) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + threads_now = lldbutil.get_threads_stopped_at_breakpoint(process, trigger_bkpt) + self.assertEqual(threads_now, initial_threads) + + def test_reverse_continue_skip_breakpoint(self): + self.reverse_continue_skip_breakpoint_internal(async_mode=False) + + def test_reverse_continue_skip_breakpoint_async(self): + self.reverse_continue_skip_breakpoint_internal(async_mode=True) + + def reverse_continue_skip_breakpoint_internal(self, async_mode): + target, process, initial_threads = self.setup_recording(async_mode) + + # Reverse-continue over a breakpoint at "trigger_breakpoint" whose + # condition is false (via function call). + # This tests that we continue in the correct direction after hitting + # the breakpoint. + trigger_bkpt = target.BreakpointCreateByName("trigger_breakpoint", None) + trigger_bkpt.SetCondition("false_condition()") + status = process.ContinueInDirection(lldb.eRunReverse) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + self.assertSuccess(status) + self.expect( + "thread list", + STOPPED_DUE_TO_HISTORY_BOUNDARY, + substrs=["stopped", "stop reason = history boundary"], + ) + + def test_continue_preserves_direction(self): + self.continue_preserves_direction_internal(async_mode=False) + + def test_continue_preserves_direction_asyhc(self): + self.continue_preserves_direction_internal(async_mode=True) + + def continue_preserves_direction_internal(self, async_mode): + target, process, initial_threads = self.setup_recording(async_mode) + + # Reverse-continue to the function "trigger_breakpoint". + trigger_bkpt = target.BreakpointCreateByName("trigger_breakpoint", None) + status = process.ContinueInDirection(lldb.eRunReverse) + self.assertSuccess(status) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + # This should continue in reverse. + status = process.Continue() + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + self.assertSuccess(status) + self.expect( + "thread list", + STOPPED_DUE_TO_HISTORY_BOUNDARY, + substrs=["stopped", "stop reason = history boundary"], + ) + + def setup_recording(self, async_mode): + """ + Record execution of code between "start_recording" and "stop_recording" breakpoints. + + Returns with the target stopped at "stop_recording", with recording disabled, + ready to reverse-execute. + """ + self.build() + target = self.dbg.CreateTarget("") + process = self.connect(target) + + # Record execution from the start of the function "start_recording" + # to the start of the function "stop_recording". We want to keep the + # interval that we record as small as possible to minimize the run-time + # of our single-stepping recorder. + start_recording_bkpt = target.BreakpointCreateByName("start_recording", None) + initial_threads = lldbutil.continue_to_breakpoint(process, start_recording_bkpt) + self.assertEqual(len(initial_threads), 1) + target.BreakpointDelete(start_recording_bkpt.GetID()) + self.start_recording() + stop_recording_bkpt = target.BreakpointCreateByName("stop_recording", None) + lldbutil.continue_to_breakpoint(process, stop_recording_bkpt) + target.BreakpointDelete(stop_recording_bkpt.GetID()) + self.stop_recording() + + self.dbg.SetAsync(async_mode) + self.expect_async_state_changes(async_mode, process, [lldb.eStateStopped]) + + return target, process, initial_threads + + def expect_async_state_changes(self, async_mode, process, states): + if not async_mode: + return + listener = self.dbg.GetListener() + lldbutil.expect_state_changes(self, listener, process, states) diff --git a/lldb/test/API/functionalities/reverse-execution/TestReverseContinueNotSupported.py b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueNotSupported.py new file mode 100644 index 00000000000000..137e9d2b617689 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueNotSupported.py @@ -0,0 +1,31 @@ +import lldb +import unittest +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +from lldbsuite.test import lldbutil + + +class TestReverseContinueNotSupported(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def test_reverse_continue_not_supported(self): + self.build() + exe = self.getBuildArtifact("a.out") + target = self.dbg.CreateTarget(exe) + self.assertTrue(target, VALID_TARGET) + + main_bkpt = target.BreakpointCreateByName("main", None) + self.assertTrue(main_bkpt, VALID_BREAKPOINT) + + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + self.assertTrue(process, PROCESS_IS_VALID) + + # This will fail gracefully. + status = process.ContinueInDirection(lldb.eRunReverse) + self.assertFailure( + status, "error: gdb-remote does not support reverse execution of processes" + ) + + self.assertSuccess(process.ContinueInDirection(lldb.eRunForward)) + self.assertState(process.GetState(), lldb.eStateExited) + self.assertEqual(process.GetExitStatus(), 0) diff --git a/lldb/test/API/functionalities/reverse-execution/TestReverseContinueWatchpoints.py b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueWatchpoints.py new file mode 100644 index 00000000000000..897220bf198071 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueWatchpoints.py @@ -0,0 +1,130 @@ +import lldb +import time +import unittest +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +from lldbsuite.test.gdbclientutils import * +from lldbsuite.test.lldbreverse import ReverseTestBase +from lldbsuite.test import lldbutil + + +class TestReverseContinueWatchpoints(ReverseTestBase): + def test_reverse_continue_watchpoint(self): + self.reverse_continue_watchpoint_internal(async_mode=False) + + def test_reverse_continue_watchpoint_async(self): + self.reverse_continue_watchpoint_internal(async_mode=True) + + def reverse_continue_watchpoint_internal(self, async_mode): + target, process, initial_threads, watch_addr = self.setup_recording(async_mode) + + error = lldb.SBError() + wp_opts = lldb.SBWatchpointOptions() + wp_opts.SetWatchpointTypeWrite(lldb.eWatchpointWriteTypeOnModify) + watchpoint = target.WatchpointCreateByAddress(watch_addr, 4, wp_opts, error) + self.assertTrue(watchpoint) + + watch_var = target.EvaluateExpression("*g_watched_var_ptr") + self.assertEqual(watch_var.GetValueAsSigned(-1), 2) + + # Reverse-continue to the function "trigger_watchpoint". + status = process.ContinueInDirection(lldb.eRunReverse) + self.assertSuccess(status) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + # We should stop at the point just before the location was modified. + watch_var = target.EvaluateExpression("*g_watched_var_ptr") + self.assertEqual(watch_var.GetValueAsSigned(-1), 1) + self.expect( + "thread list", + STOPPED_DUE_TO_WATCHPOINT, + substrs=["stopped", "trigger_watchpoint", "stop reason = watchpoint 1"], + ) + + # Stepping forward one instruction should change the value of the variable. + initial_threads[0].StepInstruction(False) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + watch_var = target.EvaluateExpression("*g_watched_var_ptr") + self.assertEqual(watch_var.GetValueAsSigned(-1), 2) + self.expect( + "thread list", + STOPPED_DUE_TO_WATCHPOINT, + substrs=["stopped", "trigger_watchpoint", "stop reason = watchpoint 1"], + ) + + def test_reverse_continue_skip_watchpoint(self): + self.reverse_continue_skip_watchpoint_internal(async_mode=False) + + def test_reverse_continue_skip_watchpoint_async(self): + self.reverse_continue_skip_watchpoint_internal(async_mode=True) + + def reverse_continue_skip_watchpoint_internal(self, async_mode): + target, process, initial_threads, watch_addr = self.setup_recording(async_mode) + + # Reverse-continue over a watchpoint whose condition is false + # (via function call). + # This tests that we continue in the correct direction after hitting + # the watchpoint. + error = lldb.SBError() + wp_opts = lldb.SBWatchpointOptions() + wp_opts.SetWatchpointTypeWrite(lldb.eWatchpointWriteTypeOnModify) + watchpoint = target.WatchpointCreateByAddress(watch_addr, 4, wp_opts, error) + self.assertTrue(watchpoint) + watchpoint.SetCondition("false_condition()") + status = process.ContinueInDirection(lldb.eRunReverse) + self.expect_async_state_changes( + async_mode, process, [lldb.eStateRunning, lldb.eStateStopped] + ) + self.assertSuccess(status) + self.expect( + "thread list", + STOPPED_DUE_TO_HISTORY_BOUNDARY, + substrs=["stopped", "stop reason = history boundary"], + ) + + def setup_recording(self, async_mode): + """ + Record execution of code between "start_recording" and "stop_recording" breakpoints. + + Returns with the target stopped at "stop_recording", with recording disabled, + ready to reverse-execute. + """ + self.build() + target = self.dbg.CreateTarget("") + process = self.connect(target) + + # Record execution from the start of the function "start_recording" + # to the start of the function "stop_recording". We want to keep the + # interval that we record as small as possible to minimize the run-time + # of our single-stepping recorder. + start_recording_bkpt = target.BreakpointCreateByName("start_recording", None) + initial_threads = lldbutil.continue_to_breakpoint(process, start_recording_bkpt) + self.assertEqual(len(initial_threads), 1) + target.BreakpointDelete(start_recording_bkpt.GetID()) + + frame0 = initial_threads[0].GetFrameAtIndex(0) + watched_var_ptr = frame0.FindValue( + "g_watched_var_ptr", lldb.eValueTypeVariableGlobal + ) + watch_addr = watched_var_ptr.GetValueAsUnsigned(0) + self.assertTrue(watch_addr > 0) + + self.start_recording() + stop_recording_bkpt = target.BreakpointCreateByName("stop_recording", None) + lldbutil.continue_to_breakpoint(process, stop_recording_bkpt) + target.BreakpointDelete(stop_recording_bkpt.GetID()) + self.stop_recording() + + self.dbg.SetAsync(async_mode) + self.expect_async_state_changes(async_mode, process, [lldb.eStateStopped]) + + return target, process, initial_threads, watch_addr + + def expect_async_state_changes(self, async_mode, process, states): + if not async_mode: + return + listener = self.dbg.GetListener() + lldbutil.expect_state_changes(self, listener, process, states) diff --git a/lldb/test/API/functionalities/reverse-execution/main.c b/lldb/test/API/functionalities/reverse-execution/main.c new file mode 100644 index 00000000000000..520e3415bf0265 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/main.c @@ -0,0 +1,25 @@ +int false_condition() { return 0; } + +int *g_watched_var_ptr; + +static void start_recording() {} + +static void trigger_watchpoint() { *g_watched_var_ptr = 2; } + +static void trigger_breakpoint() {} + +static void stop_recording() {} + +int main() { + // The watched memory location is on the stack because + // that's what our reverse execution engine records and + // replays. + int watched_var = 1; + g_watched_var_ptr = &watched_var; + + start_recording(); + trigger_watchpoint(); + trigger_breakpoint(); + stop_recording(); + return 0; +} diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp index 6ca4dfb4711a13..57e6b254771f11 100644 --- a/lldb/tools/lldb-dap/JSONUtils.cpp +++ b/lldb/tools/lldb-dap/JSONUtils.cpp @@ -996,6 +996,9 @@ llvm::json::Value CreateThreadStopped(DAP &dap, lldb::SBThread &thread, case lldb::eStopReasonProcessorTrace: body.try_emplace("reason", "processor trace"); break; + case lldb::eStopReasonHistoryBoundary: + body.try_emplace("reason", "history boundary"); + break; case lldb::eStopReasonSignal: case lldb::eStopReasonException: body.try_emplace("reason", "exception"); diff --git a/lldb/tools/lldb-dap/LLDBUtils.cpp b/lldb/tools/lldb-dap/LLDBUtils.cpp index 48b63b59e0e3fe..16ca3d779dfea0 100644 --- a/lldb/tools/lldb-dap/LLDBUtils.cpp +++ b/lldb/tools/lldb-dap/LLDBUtils.cpp @@ -113,6 +113,7 @@ bool ThreadHasStopReason(lldb::SBThread &thread) { case lldb::eStopReasonVFork: case lldb::eStopReasonVForkDone: case lldb::eStopReasonInterrupt: + case lldb::eStopReasonHistoryBoundary: return true; case lldb::eStopReasonThreadExiting: case lldb::eStopReasonInvalid: _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits