From 0caba6c8dc2f6f0da61f30c169f59d40591cddbc Mon Sep 17 00:00:00 2001 From: David Spickett Date: Thu, 30 Jan 2025 14:03:01 +0000 Subject: [PATCH] Reland "[lldb] Implement basic support for reverse-continue" (#123906)" (#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. I have also added skipIfRemote to the tests, since getting them working in that context is too complex to be worth it. Memory write failures now display the range they tried to write, and register write errors will show the name of the register where possible. The patch also includes a workaround for a an issue where the test code could mistake an `x` response that happens to begin with an `O` for an output packet (stdout). This workaround will not be necessary one we start using the [new implementation](https://discourse.llvm.org/t/rfc-fixing-incompatibilties-of-the-x-packet-w-r-t-gdb/84288) of the `x` packet. --------- Co-authored-by: Pavel Labath --- 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 | 541 ++++++++++++++++++ .../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 | 157 +++++ .../TestReverseContinueNotSupported.py | 31 + .../TestReverseContinueWatchpoints.py | 134 +++++ .../functionalities/reverse-execution/main.c | 25 + lldb/tools/lldb-dap/JSONUtils.cpp | 3 + lldb/tools/lldb-dap/LLDBUtils.cpp | 1 + 40 files changed, 1394 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 1624e02070b1b..882b8bd837131 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 a184e6dd891af..b14eb3fbd91d0 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, /// 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, /// \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 45beac129e86f..9a13371708be5 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 { 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 { 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 ef66fa11574db..cd82ee7d75603 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, /// 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 f931bb83a8cea..c796975de6015 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 d6da484f4fc13..a7bac8cc5ecf6 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, public UserID { @@ -497,6 +506,10 @@ class ThreadPlan : public std::enable_shared_from_this, 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 5c44b9fb17b27..f4418d779a4da 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 50d2233509de6..5f12e648684d7 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 1784487323ad6..732d617132068 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 0000000000000..a84c80f155a0a --- /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 0000000000000..a42cc7cac15d3 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/lldbreverse.py @@ -0,0 +1,541 @@ +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, name, bitsize, little_endian): + self.lldb_index = lldb_index + self.name = name + 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] == "x": + # Suppress *binary* reads as results starting with "O" can be mistaken for an output packet + # by the test server code + 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": + try: + reg_name = self.general_purpose_register_info[lldb_index].name + except KeyError: + reg_name = f"with index {lldb_index}" + raise ValueError(f"Can't restore thread register {reg_name}") + 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( + f"Can't restore memory block ranging from 0x{block.address:x} to 0x{block.address+len(block.data):x}." + ) + 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, info["name"], 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 81b286340560d..b0d2a27dba0ee 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 94376a16d39f6..fc552ef887ce5 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 9773144723c34..23ea449b30cca 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 cc848076dab5f..d9469fc1390d6 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 764dcfd1903b1..284955a65a442 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 a6d6a78357fe5..f6aaff2fb4e92 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(stop_info.reason)); diff --git a/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp b/lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp index 9b2907c680996..ef57e7bfd1e42 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 e5ec5914f9600..1b71d83f70b08 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 1bdacec221695..7ff32ee96e004 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 e97cfb790248b..97284b7cd1436 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 b3f1c6f052955..adc311ce2dd28 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 898d176abc346..116b47c1edf03 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 8cdeaac5c7cb2..89d2730cfccd0 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 538c868014009..fa511af9b6d54 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(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 2492795851388..1cbd1e82b381d 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 d2111ce877ce5..3360bd9a044bd 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 0335364b4010b..8ebe4ca5f3d44 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 89731f798deda..c594fd94c9904 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 356917a45b7b3..355d3a9ad6e8f 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 b526131097061..2c4d925c73222 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 6cbef330bf488..99e2c1204146a 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 dfd2157e70d4a..09437b0048c2c 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 0000000000000..10495940055b6 --- /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 0000000000000..537da90586777 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py @@ -0,0 +1,157 @@ +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): + @skipIfRemote + def test_reverse_continue(self): + self.reverse_continue_internal(async_mode=False) + + @skipIfRemote + 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) + + @skipIfRemote + def test_reverse_continue_breakpoint(self): + self.reverse_continue_breakpoint_internal(async_mode=False) + + @skipIfRemote + 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) + + @skipIfRemote + def test_reverse_continue_skip_breakpoint(self): + self.reverse_continue_skip_breakpoint_internal(async_mode=False) + + @skipIfRemote + 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"], + ) + + @skipIfRemote + def test_continue_preserves_direction(self): + self.continue_preserves_direction_internal(async_mode=False) + + @skipIfRemote + 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 0000000000000..137e9d2b61768 --- /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 0000000000000..8a6f35cc8e508 --- /dev/null +++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueWatchpoints.py @@ -0,0 +1,134 @@ +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): + @skipIfRemote + def test_reverse_continue_watchpoint(self): + self.reverse_continue_watchpoint_internal(async_mode=False) + + @skipIfRemote + 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"], + ) + + @skipIfRemote + def test_reverse_continue_skip_watchpoint(self): + self.reverse_continue_skip_watchpoint_internal(async_mode=False) + + @skipIfRemote + 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 0000000000000..520e3415bf026 --- /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 6ca4dfb4711a1..57e6b254771f1 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 48b63b59e0e3f..16ca3d779dfea 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: