[Lldb-commits] [lldb] [lldb] Implement basic support for reverse-continue (PR #112079)

via lldb-commits lldb-commits at lists.llvm.org
Fri Oct 11 21:51:30 PDT 2024


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Robert O'Callahan (rocallahan)

<details>
<summary>Changes</summary>

This commit only adds support for the
`SBProcess::ReverseContinue()` API. A user-accessible command for this will follow in a later commit.

This feature depends on a gdbserver implementation (e.g. `rr`) providing support for the `bc` and `bs` packets. `lldb-server` does not support those packets, and there is no plan to change that. So, for testing purposes, `lldbreverse.py` wraps `lldb-server` with a Python implementation of *very limited* record-and-replay functionality.

---

Patch is 65.16 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/112079.diff


32 Files Affected:

- (modified) lldb/include/lldb/API/SBProcess.h (+1) 
- (modified) lldb/include/lldb/Target/Process.h (+15-6) 
- (modified) lldb/include/lldb/Target/StopInfo.h (+6) 
- (modified) lldb/include/lldb/lldb-enumerations.h (+6) 
- (modified) lldb/packages/Python/lldbsuite/test/gdbclientutils.py (+3-2) 
- (added) lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py (+175) 
- (added) lldb/packages/Python/lldbsuite/test/lldbreverse.py (+418) 
- (modified) lldb/packages/Python/lldbsuite/test/lldbtest.py (+2) 
- (modified) lldb/source/API/SBProcess.cpp (+6-2) 
- (modified) lldb/source/API/SBThread.cpp (+2) 
- (modified) lldb/source/Interpreter/CommandInterpreter.cpp (+2-1) 
- (modified) lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp (+3) 
- (modified) lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.cpp (+8-1) 
- (modified) lldb/source/Plugins/Process/MacOSX-Kernel/ProcessKDP.h (+1-1) 
- (modified) lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp (+7-1) 
- (modified) lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h (+1-1) 
- (modified) lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp (+22) 
- (modified) lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h (+6) 
- (modified) lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp (+1) 
- (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp (+63-14) 
- (modified) lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h (+1-1) 
- (modified) lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp (+7-2) 
- (modified) lldb/source/Plugins/Process/scripted/ScriptedProcess.h (+1-1) 
- (modified) lldb/source/Target/Process.cpp (+20-9) 
- (modified) lldb/source/Target/StopInfo.cpp (+29) 
- (modified) lldb/source/Target/Thread.cpp (+6-2) 
- (added) lldb/test/API/functionalities/reverse-execution/Makefile (+3) 
- (added) lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py (+115) 
- (added) lldb/test/API/functionalities/reverse-execution/TestReverseContinueNotSupported.py (+30) 
- (added) lldb/test/API/functionalities/reverse-execution/main.c (+14) 
- (modified) lldb/tools/lldb-dap/JSONUtils.cpp (+3) 
- (modified) lldb/tools/lldb-dap/LLDBUtils.cpp (+1) 


``````````diff
diff --git a/lldb/include/lldb/API/SBProcess.h b/lldb/include/lldb/API/SBProcess.h
index 1624e02070b1b2..8b8ed830b54cc0 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 Continue(RunDirection direction);
 
   lldb::SBError Stop();
 
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index b8c53a474ba6b9..fe7fbc50fd5770 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -857,10 +857,10 @@ class Process : public std::enable_shared_from_this<Process>,
   /// \see Thread:Resume()
   /// \see Thread:Step()
   /// \see Thread:Suspend()
-  Status Resume();
+  Status Resume(lldb::RunDirection direction = lldb::eRunForward);
 
   /// Resume a process, and wait for it to stop.
-  Status ResumeSynchronous(Stream *stream);
+  Status ResumeSynchronous(Stream *stream, lldb::RunDirection direction = lldb::eRunForward);
 
   /// Halts a running process.
   ///
@@ -1104,9 +1104,14 @@ class Process : public std::enable_shared_from_this<Process>,
   /// \see Thread:Resume()
   /// \see Thread:Step()
   /// \see Thread:Suspend()
-  virtual Status DoResume() {
-    return Status::FromErrorStringWithFormatv(
-        "error: {0} does not support resuming processes", GetPluginName());
+  virtual Status DoResume(lldb::RunDirection direction) {
+    if (direction == lldb::RunDirection::eRunForward) {
+      return Status::FromErrorStringWithFormatv(
+          "error: {0} does not support resuming processes", GetPluginName());
+    } else {
+      return Status::FromErrorStringWithFormatv(
+          "error: {0} does not support reverse execution of processes", GetPluginName());
+    }
   }
 
   /// Called after resuming a process.
@@ -2332,6 +2337,8 @@ class Process : public std::enable_shared_from_this<Process>,
 
   bool IsRunning() const;
 
+  lldb::RunDirection GetLastRunDirection() { return m_last_run_direction; }
+
   DynamicCheckerFunctions *GetDynamicCheckers() {
     return m_dynamic_checkers_up.get();
   }
@@ -2851,7 +2858,7 @@ void PruneThreadPlans();
   ///
   /// \return
   ///     An Status object describing the success or failure of the resume.
-  Status PrivateResume();
+  Status PrivateResume(lldb::RunDirection direction = lldb::eRunForward);
 
   // Called internally
   void CompleteAttach();
@@ -3127,6 +3134,8 @@ void PruneThreadPlans();
                            // m_currently_handling_do_on_removals are true,
                            // Resume will only request a resume, using this
                            // flag to check.
+  // The direction of execution from the last time this process was resumed.
+  lldb::RunDirection m_last_run_direction;
 
   lldb::tid_t m_interrupt_tid; /// The tid of the thread that issued the async
                                /// interrupt, used by thread plan timeout. It
diff --git a/lldb/include/lldb/Target/StopInfo.h b/lldb/include/lldb/Target/StopInfo.h
index fae90364deaf0a..072f71f6b1122f 100644
--- a/lldb/include/lldb/Target/StopInfo.h
+++ b/lldb/include/lldb/Target/StopInfo.h
@@ -142,6 +142,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/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index 938f6e3abe8f2a..232d1dfdb5c9d0 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..2a9592bf4545a4
--- /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__)
+
+        if len(self.logger.handlers) > 0:
+            return  # We have set up this handler already
+
+        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)
+
+        lldb_server_exe = lldbgdbserverutils.get_lldb_server_exe()
+        if lldb_server_exe is None:
+            self.debug_monitor_exe = lldbgdbserverutils.get_debugserver_exe()
+            self.assertTrue(self.debug_monitor_exe is not None)
+            self.debug_monitor_extra_args = []
+        else:
+            self.debug_monitor_exe = lldb_server_exe
+            self.debug_monitor_extra_args = ["gdbserver"]
+
+        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 get_next_port(self):
+        return 12000 + random.randint(0, 3999)
+
+    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.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)
+        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..0f02fdffbdeada
--- /dev/null
+++ b/lldb/packages/Python/lldbsuite/test/lldbreverse.py
@@ -0,0 +1,418 @@
+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).
+    """
+
+    """
+    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
+
+    pid = 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", ":"):
+            reply = self.pass_through(packet)
+            return reply + ";ReverseStep+;ReverseContinue+"
+        if self.is_command(packet, "vCont", ";"):
+            if self.recording_enabled:
+                return self.continue_with_recording(packet)
+            snapshots = []
+        if packet[0] == "c" or packet[0] == "s" or packet[0] == "C" or packet[0] == "S":
+            raise ValueError("LLDB should not be sending old-style continuation packets")
+        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 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)
+            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 in ["name", "threads", "thread-pcs", "reason"]:
+                    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(snaps...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/112079


More information about the lldb-commits mailing list