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

Robert O'Callahan via lldb-commits lldb-commits at lists.llvm.org
Sat Jul 20 06:00:04 PDT 2024


https://github.com/rocallahan updated https://github.com/llvm/llvm-project/pull/99736

>From 25e184d1f0d06c09726ef72c0b3f1966522ea3df Mon Sep 17 00:00:00 2001
From: Robert O'Callahan <robert at ocallahan.org>
Date: Fri, 19 Jul 2024 22:46:42 +1200
Subject: [PATCH] [lldb] Implement basic support for reverse-continue

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.
---
 lldb/include/lldb/API/SBProcess.h             |   2 +
 lldb/include/lldb/Target/Process.h            |  25 +-
 lldb/include/lldb/Target/StopInfo.h           |   3 +
 lldb/include/lldb/lldb-enumerations.h         |   6 +
 .../Python/lldbsuite/test/gdbclientutils.py   |   5 +-
 .../Python/lldbsuite/test/lldbgdbproxy.py     | 176 ++++++++
 .../Python/lldbsuite/test/lldbreverse.py      | 418 ++++++++++++++++++
 .../Python/lldbsuite/test/lldbtest.py         |   2 +
 lldb/source/API/SBProcess.cpp                 |  20 +
 lldb/source/API/SBThread.cpp                  |   2 +
 .../source/Interpreter/CommandInterpreter.cpp |   3 +-
 .../Process/Linux/NativeThreadLinux.cpp       |   3 +
 .../GDBRemoteCommunicationClient.cpp          |  22 +
 .../gdb-remote/GDBRemoteCommunicationClient.h |   6 +
 .../GDBRemoteCommunicationServerLLGS.cpp      |   1 +
 .../Process/gdb-remote/ProcessGDBRemote.cpp   |  94 ++++
 .../Process/gdb-remote/ProcessGDBRemote.h     |   2 +
 lldb/source/Target/Process.cpp                |  29 +-
 lldb/source/Target/StopInfo.cpp               |  29 ++
 lldb/source/Target/Thread.cpp                 |   8 +-
 .../reverse-execution/Makefile                |   3 +
 .../TestReverseContinueBreakpoints.py         | 119 +++++
 .../functionalities/reverse-execution/main.c  |  14 +
 lldb/tools/lldb-dap/JSONUtils.cpp             |   3 +
 lldb/tools/lldb-dap/LLDBUtils.cpp             |   1 +
 25 files changed, 979 insertions(+), 17 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/main.c

diff --git a/lldb/include/lldb/API/SBProcess.h b/lldb/include/lldb/API/SBProcess.h
index 778be79583990..9b17bac0093e6 100644
--- a/lldb/include/lldb/API/SBProcess.h
+++ b/lldb/include/lldb/API/SBProcess.h
@@ -160,6 +160,8 @@ class LLDB_API SBProcess {
 
   lldb::SBError Continue();
 
+  lldb::SBError ReverseContinue();
+
   lldb::SBError Stop();
 
   lldb::SBError Kill();
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index c8475db8ae160..203d3484f3704 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -874,10 +874,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.
   ///
@@ -1136,6 +1136,22 @@ class Process : public std::enable_shared_from_this<Process>,
     return error;
   }
 
+  /// Like DoResume() but executes in reverse if supported.
+  ///
+  /// \return
+  ///     Returns \b true if the process successfully resumes using
+  ///     the thread run control actions, \b false otherwise.
+  ///
+  /// \see Thread:Resume()
+  /// \see Thread:Step()
+  /// \see Thread:Suspend()
+  virtual Status DoResumeReverse() {
+    Status error;
+    error.SetErrorStringWithFormatv(
+        "error: {0} does not support reverse execution of processes", GetPluginName());
+    return error;
+  }
+
   /// Called after resuming a process.
   ///
   /// Allow Process plug-ins to execute some code after resuming a process.
@@ -2367,6 +2383,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();
   }
@@ -2861,7 +2879,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();
@@ -3139,6 +3157,7 @@ void PruneThreadPlans();
                            // m_currently_handling_do_on_removals are true,
                            // Resume will only request a resume, using this
                            // flag to check.
+  lldb::RunDirection m_last_run_direction;
 
   /// This is set at the beginning of Process::Finalize() to stop functions
   /// from looking up or creating things during or after a finalize call.
diff --git a/lldb/include/lldb/Target/StopInfo.h b/lldb/include/lldb/Target/StopInfo.h
index d1848fcbbbdb1..f49854275653e 100644
--- a/lldb/include/lldb/Target/StopInfo.h
+++ b/lldb/include/lldb/Target/StopInfo.h
@@ -138,6 +138,9 @@ class StopInfo : public std::enable_shared_from_this<StopInfo> {
   static lldb::StopInfoSP
   CreateStopReasonProcessorTrace(Thread &thread, const char *description);
 
+  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 74ff458bf87de..5f1823ae04560 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,
@@ -253,6 +256,9 @@ enum StopReason {
   eStopReasonFork,
   eStopReasonVFork,
   eStopReasonVForkDone,
+  // 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..45018e7daa7df
--- /dev/null
+++ b/lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py
@@ -0,0 +1,176 @@
+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)
+
+        self.port = self.get_next_port()
+        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 0000000000000..0f02fdffbdead
--- /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(snapshot)
+            if requested_step:
+                self.logger.debug("Recording stopped for step")
+                return reply
+
+    def parse_stop(self, reply):
+        result = {}
+        if not reply:
+            raise ValueError("Invalid empty packet")
+        if reply[0] == "T" and len(reply) >= 3:
+            result = {k:v for k, v in self.parse_pairs(reply[3:])}
+            return (int(reply[1:3], 16), result)
+        raise "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)
+            memory += self.read_memory(thread_sp - BELOW_STACK_POINTER, thread_sp + ABOVE_STACK_POINTER)
+            thread_snapshots.append(thread_snapshot)
+        self.set_current_thread(current_thread)
+        return StateSnapshot(thread_snapshots, memory)
+
+    def restore_snapshot(self, snapshot):
+        """
+        Restore the snapshot during reverse execution.
+
+        If this triggers a breakpoint or watchpoint, return the stop reply,
+        otherwise None.
+        """
+        current_thread = self.get_current_thread()
+        stop_reasons = []
+        for thread_snapshot in snapshot.thread_snapshots:
+            thread_id = thread_snapshot.thread_id
+            for lldb_index in sorted(thread_snapshot.registers.keys()):
+                data = thread_snapshot.registers[lldb_index]
+                reply = self.pass_through(f"P{lldb_index:x}={data};thread:{thread_id:x};")
+                if reply != "OK":
+                    raise ValueError("Can't restore thread register")
+            if thread_id == snapshot.thread_id:
+                new_pc = self.get_register(self.pc_register_info, thread_snapshot.registers)
+                if self.breakpoint_triggered_at(new_pc):
+                    stop_reasons.append([("reason", "breakpoint")])
+        self.set_current_thread(current_thread)
+        for block in snapshot.memory:
+            current_memory = self.pass_through(f"m{block.address:x},{(len(block.data)/2):x}")
+            if not current_memory or current_memory[0] == 'E':
+                raise ValueError("Can't read back memory")
+            reply = self.pass_through(f"M{block.address:x},{len(block.data)/2:x}:" + block.data)
+            if reply != "OK":
+                raise ValueError("Can't restore memory")
+            watch_addr = self.watchpoint_triggered(block, current_memory[1:])
+            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:{self.pid:x}.{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("(p([0-9a-f]+)[.])?([0-9a-f]+)$", thread_id)
+        if m is None:
+            raise ValueError("Invalid thread ID: " + thread_id)
+        if self.pid is None:
+            self.pid = int(m.group(2), 16)
+        return int(m.group(3), 16)
+
+    def history_boundary_reply(self, thread_id):
+        return f"T00thread:{self.pid:x}.{thread_id:x};replaylog:begin;"
+
+    def singlestep_stop_reply(self, thread_id):
+        return f"T05thread:{self.pid:x}.{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{self.pid:x}.{thread_id:x}")
+            self.pass_through(f"Hc{self.pid:x}.{thread_id:x}")
+        else:
+            self.pass_through(f"Hc-1.-1")
+            self.pass_through(f"Hg-1.-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 read_memory(self, start_addr, end_addr):
+        """
+        Read a region of memory from the target.
+
+        Some of the addresses may extend into invalid virtual memory;
+        skip those areas.
+        Return a list of blocks containing the valid area(s) in the
+        requested range.
+        """
+        regions = []
+        start_addr = start_addr & (BLOCK_SIZE - 1)
+        end_addr = (end_addr + BLOCK_SIZE - 1) & (BLOCK_SIZE - 1)
+        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[1:])
+                regions.append(block)
+        return regions
+
+    def ensure_register_info(self):
+        if self.general_purpose_register_info is not None:
+            return
+        reply = self.pass_through("qHostInfo")
+        little_endian = any(kv == ("endian", "little") for kv in self.parse_pairs(reply))
+        self.general_purpose_register_info = {}
+        lldb_index = 0
+        while True:
+            reply = self.pass_through(f"qRegisterInfo{lldb_index:x}")
+            if not reply or reply[0] == 'E':
+                break
+            info = {k:v for k, v in self.parse_pairs(reply)}
+            reg_info = RegisterInfo(lldb_index, int(info["bitsize"]), little_endian)
+            if info["set"] == "General Purpose Registers" and not "container-regs" in info:
+                self.general_purpose_register_info[lldb_index] = reg_info
+            if "generic" in info:
+                if info["generic"] == "pc":
+                    self.pc_register_info = reg_info
+                elif info["generic"] == "sp":
+                    self.sp_register_info = reg_info
+            lldb_index += 1
+        if self.pc_register_info is None or self.sp_register_info is None:
+            raise ValueError("Can't find generic pc or sp register")
+
+    def get_thread_list(self):
+        threads = []
+        reply = self.pass_through("qfThreadInfo")
+        while True:
+            if not reply:
+                raise ValueError("Missing reply packet")
+            if reply[0] == 'm':
+                for id in reply[1:].split(","):
+                    threads.append(self.parse_thread_id(id))
+            elif reply[0] == 'l':
+                return threads
+            reply = self.pass_through("qsThreadInfo")
diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py
index 5e50b0c145885..166cc83c7093d 100644
--- a/lldb/packages/Python/lldbsuite/test/lldbtest.py
+++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py
@@ -144,6 +144,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/source/API/SBProcess.cpp b/lldb/source/API/SBProcess.cpp
index b88f897ff5280..5ad2ffae22df0 100644
--- a/lldb/source/API/SBProcess.cpp
+++ b/lldb/source/API/SBProcess.cpp
@@ -582,6 +582,26 @@ SBError SBProcess::Continue() {
   return sb_error;
 }
 
+SBError SBProcess::ReverseContinue() {
+  LLDB_INSTRUMENT_VA(this);
+
+  SBError sb_error;
+  ProcessSP process_sp(GetSP());
+
+  if (process_sp) {
+    std::lock_guard<std::recursive_mutex> guard(
+        process_sp->GetTarget().GetAPIMutex());
+
+    if (process_sp->GetTarget().GetDebugger().GetAsyncExecution())
+      sb_error.ref() = process_sp->Resume(RunDirection::eRunReverse);
+    else
+      sb_error.ref() = process_sp->ResumeSynchronous(nullptr, RunDirection::eRunReverse);
+  } else
+    sb_error.SetErrorString("SBProcess is invalid");
+
+  return sb_error;
+}
+
 SBError SBProcess::Destroy() {
   LLDB_INSTRUMENT_VA(this);
 
diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp
index 53643362421d4..7ae1324e493d9 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;
 
@@ -230,6 +231,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 fc07168b6c0ac..a659f93d15bce 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -2513,7 +2513,8 @@ bool CommandInterpreter::DidProcessStopAbnormally() const {
     const StopReason reason = stop_info->GetStopReason();
     if (reason == eStopReasonException ||
         reason == eStopReasonInstrumentation ||
-        reason == eStopReasonProcessorTrace)
+        reason == eStopReasonProcessorTrace ||
+        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 26cb26daabf52..1b23069a5530e 100644
--- a/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp
+++ b/lldb/source/Plugins/Process/Linux/NativeThreadLinux.cpp
@@ -82,6 +82,9 @@ void LogThreadStopInfo(Log &log, const ThreadStopInfo &stop_info,
   case eStopReasonProcessorTrace:
     log.Printf("%s: %s processor trace", __FUNCTION__, header);
     return;
+  case eStopReasonHistoryBoundary:
+    log.Printf("%s: %s history boundary", __FUNCTION__, header);
+    return;
   default:
     log.Printf("%s: %s invalid stop reason %" PRIu32, __FUNCTION__, header,
                static_cast<uint32_t>(stop_info.reason));
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
index 74e392249a94e..c0c599161b992 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
@@ -197,6 +197,20 @@ 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;
@@ -293,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;
@@ -346,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
@@ -396,6 +414,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 08d5f5039d516..9c7431028de1c 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -714,6 +714,7 @@ static const char *GetStopReasonString(StopReason stop_reason) {
     return "vfork";
   case eStopReasonVForkDone:
     return "vforkdone";
+  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 604c92369e9a2..0df49ac6c1f33 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -1395,6 +1395,94 @@ Status ProcessGDBRemote::DoResume() {
   return error;
 }
 
+Status ProcessGDBRemote::DoResumeReverse() {
+  Status error;
+  Log *log = GetLog(GDBRLog::Process);
+  LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse()");
+
+  ListenerSP listener_sp(
+      Listener::MakeListener("gdb-remote.resume-packet-sent"));
+  if (listener_sp->StartListeningForEvents(
+          &m_gdb_comm, GDBRemoteClientBase::eBroadcastBitRunPacketSent)) {
+    listener_sp->StartListeningForEvents(
+        &m_async_broadcaster,
+        ProcessGDBRemote::eBroadcastBitAsyncThreadDidExit);
+
+    const size_t num_threads = GetThreadList().GetSize();
+
+    StreamString continue_packet;
+
+    // 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();
+    // We can only `bc` to reverse-continue all threads,
+    // or `bs` to reverse-step the current thread (which may also
+    // reverse-continue other threads by some amount).
+    // These packets do not support delivering signals.
+    if (num_continue_C_tids > 0 || num_continue_S_tids > 0) {
+      error.SetErrorString("can't deliver signals while running in reverse");
+      LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: Signals not supported");
+      return error;
+    }
+
+    if (num_continue_s_tids > 0) {
+      if (num_continue_s_tids > 1) {
+        error.SetErrorString("can't step multiple threads while reverse-stepping");
+        LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: can't step multiple threads");
+        return error;
+      }
+
+      if (!m_gdb_comm.GetReverseStepSupported()) {
+        error.SetErrorString("target does not support reverse-stepping");
+        LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: target does not support reverse-stepping");
+        return error;
+      }
+
+      m_gdb_comm.SetCurrentThreadForRun(m_continue_s_tids.front());
+      continue_packet.PutCString("bs");
+    } else {
+      if (!m_gdb_comm.GetReverseContinueSupported()) {
+        error.SetErrorString("target does not support reverse-continue");
+        LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: target does not support reverse-continue");
+        return error;
+      }
+
+      // All threads continue whether requested or not ---
+      // we can't change how threads ran in the past.
+      continue_packet.PutCString("bc");
+    }
+
+    EventSP event_sp;
+    if (!m_async_thread.IsJoinable()) {
+      error.SetErrorString("Trying to resume but the async thread is dead.");
+      LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: Trying to resume but the "
+                     "async thread is dead.");
+      return error;
+    }
+
+    auto data_sp =
+        std::make_shared<EventDataBytes>(continue_packet.GetString());
+    m_async_broadcaster.BroadcastEvent(eBroadcastBitAsyncContinue, data_sp);
+
+    if (!listener_sp->GetEvent(event_sp, std::chrono::seconds(5))) {
+      error.SetErrorString("Resume timed out.");
+      LLDB_LOGF(log, "ProcessGDBRemote::DoResumeReverse: Resume timed out.");
+    } else if (event_sp->BroadcasterIs(&m_async_broadcaster)) {
+      error.SetErrorString("Broadcast continue, but the async thread was "
+                           "killed before we got an ack back.");
+      LLDB_LOGF(log,
+                "ProcessGDBRemote::DoResumeReverse: Broadcast continue, but the "
+                "async thread was killed before we got an ack back.");
+      return error;
+    }
+  }
+
+  return error;
+}
+
 void ProcessGDBRemote::ClearThreadIDList() {
   std::lock_guard<std::recursive_mutex> guard(m_thread_list_real.GetMutex());
   m_thread_ids.clear();
@@ -1851,6 +1939,10 @@ ThreadSP ProcessGDBRemote::SetThreadStopInfo(
           thread_sp->SetStopInfo(StopInfo::CreateStopReasonWithException(
               *thread_sp, description.c_str()));
           handled = true;
+        } else if (reason == "replaylog") {
+          thread_sp->SetStopInfo(StopInfo::CreateStopReasonHistoryBoundary(
+              *thread_sp, description.c_str()));
+          handled = true;
         } else if (reason == "exec") {
           did_exec = true;
           thread_sp->SetStopInfo(
@@ -2275,6 +2367,8 @@ StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) {
         StreamString ostr;
         ostr.Printf("%" PRIu64, wp_addr);
         description = std::string(ostr.GetString());
+      } else if (key.compare("replaylog") == 0) {
+        reason = "replaylog";
       } 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 b44ffefcd0d36..9b47010f2ece0 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -113,6 +113,8 @@ class ProcessGDBRemote : public Process,
 
   Status DoResume() override;
 
+  Status DoResumeReverse() override;
+
   Status DoHalt(bool &caused_stop) override;
 
   Status DoDetach(bool keep_stopped) override;
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index d5a639d9beacd..63433a3471895 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -473,7 +473,8 @@ Process::Process(lldb::TargetSP target_sp, ListenerSP listener_sp,
       m_memory_cache(*this), m_allocated_memory_cache(*this),
       m_should_detach(false), m_next_event_action_up(), m_public_run_lock(),
       m_private_run_lock(), m_currently_handling_do_on_removals(false),
-      m_resume_requested(false), m_finalizing(false), m_destructing(false),
+      m_resume_requested(false), m_last_run_direction(eRunForward),
+      m_finalizing(false), m_destructing(false),
       m_clear_thread_plans_on_stop(false), m_force_next_event_delivery(false),
       m_last_broadcast_state(eStateInvalid), m_destroy_in_process(false),
       m_can_interpret_function_calls(false), m_run_thread_plan_lock(),
@@ -870,6 +871,7 @@ bool Process::HandleProcessStateChangedEvent(
             switch (thread_stop_reason) {
             case eStopReasonInvalid:
             case eStopReasonNone:
+            case eStopReasonHistoryBoundary:
               break;
 
             case eStopReasonSignal: {
@@ -1376,7 +1378,7 @@ void Process::SetPublicState(StateType new_state, bool restarted) {
   }
 }
 
-Status Process::Resume() {
+Status Process::Resume(RunDirection direction) {
   Log *log(GetLog(LLDBLog::State | LLDBLog::Process));
   LLDB_LOGF(log, "(plugin = %s) -- locking run lock", GetPluginName().data());
   if (!m_public_run_lock.TrySetRunning()) {
@@ -1385,7 +1387,7 @@ Status Process::Resume() {
              GetPluginName().data());
     return error;
   }
-  Status error = PrivateResume();
+  Status error = PrivateResume(direction);
   if (!error.Success()) {
     // Undo running state change
     m_public_run_lock.SetStopped();
@@ -1393,7 +1395,7 @@ Status Process::Resume() {
   return error;
 }
 
-Status Process::ResumeSynchronous(Stream *stream) {
+Status Process::ResumeSynchronous(Stream *stream, RunDirection direction) {
   Log *log(GetLog(LLDBLog::State | LLDBLog::Process));
   LLDB_LOGF(log, "Process::ResumeSynchronous -- locking run lock");
   if (!m_public_run_lock.TrySetRunning()) {
@@ -1406,7 +1408,7 @@ Status Process::ResumeSynchronous(Stream *stream) {
       Listener::MakeListener(ResumeSynchronousHijackListenerName.data()));
   HijackProcessEvents(listener_sp);
 
-  Status error = PrivateResume();
+  Status error = PrivateResume(direction);
   if (error.Success()) {
     StateType state =
         WaitForProcessToStop(std::nullopt, nullptr, true, listener_sp, stream,
@@ -3252,7 +3254,7 @@ Status Process::ConnectRemote(llvm::StringRef remote_url) {
   return error;
 }
 
-Status Process::PrivateResume() {
+Status Process::PrivateResume(RunDirection direction) {
   Log *log(GetLog(LLDBLog::Process | LLDBLog::Step));
   LLDB_LOGF(log,
             "Process::PrivateResume() m_stop_id = %u, public state: %s "
@@ -3264,6 +3266,11 @@ Status Process::PrivateResume() {
   // filters before resuming.
   UpdateAutomaticSignalFiltering();
 
+  if (m_last_run_direction != direction) {
+    m_thread_list.DiscardThreadPlans();
+    m_last_run_direction = direction;
+  }
+
   Status error(WillResume());
   // Tell the process it is about to resume before the thread list
   if (error.Success()) {
@@ -3281,7 +3288,11 @@ Status Process::PrivateResume() {
             "Process::PrivateResume PreResumeActions failed, not resuming.");
       } else {
         m_mod_id.BumpResumeID();
-        error = DoResume();
+        if (direction == eRunForward) {
+          error = DoResume();
+        } else {
+          error = DoResumeReverse();
+        }
         if (error.Success()) {
           DidResume();
           m_thread_list.DidResume();
@@ -3714,7 +3725,7 @@ bool Process::ShouldBroadcastEvent(Event *event_ptr) {
                     "from state: %s",
                     static_cast<void *>(event_ptr), StateAsCString(state));
           ProcessEventData::SetRestartedInEvent(event_ptr, true);
-          PrivateResume();
+          PrivateResume(m_last_run_direction);
         }
       } else {
         return_value = true;
@@ -4316,7 +4327,7 @@ void Process::ProcessEventData::DoOnRemoval(Event *event_ptr) {
     SetRestarted(true);
     // Use the private resume method here, since we aren't changing the run
     // lock state.
-    process_sp->PrivateResume();
+    process_sp->PrivateResume(process_sp->m_last_run_direction);
   } else {
     bool hijacked = process_sp->IsHijackedForEvent(eBroadcastBitStateChanged) &&
                     !process_sp->StateChangedIsHijackedForSynchronousResume();
diff --git a/lldb/source/Target/StopInfo.cpp b/lldb/source/Target/StopInfo.cpp
index 95f78056b1644..aece2b99d7a02 100644
--- a/lldb/source/Target/StopInfo.cpp
+++ b/lldb/source/Target/StopInfo.cpp
@@ -1189,6 +1189,30 @@ 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";
+    else
+      return m_description.c_str();
+  }
+};
+
 // StopInfoThreadPlan
 
 class StopInfoThreadPlan : public StopInfo {
@@ -1411,6 +1435,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 e75f5a356cec2..3a1e950f65e55 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -618,10 +618,12 @@ void 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 &&
+        process_sp->GetLastRunDirection() == 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.
@@ -1715,6 +1717,8 @@ std::string Thread::StopReasonAsString(lldb::StopReason reason) {
     return "instrumentation break";
   case eStopReasonProcessorTrace:
     return "processor trace";
+  case eStopReasonHistoryBoundary:
+    return "history boundary";
   }
 
   return "StopReason = " + std::to_string(reason);
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..bd449e5652560
--- /dev/null
+++ b/lldb/test/API/functionalities/reverse-execution/TestReverseContinueBreakpoints.py
@@ -0,0 +1,119 @@
+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):
+    @add_test_categories(["dwarf"])
+    def test_reverse_continue(self):
+        self.reverse_continue_internal(async_mode=False)
+
+    @add_test_categories(["dwarf"])
+    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.ReverseContinue()
+        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.Continue()
+        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)
+
+    @add_test_categories(["dwarf"])
+    def test_reverse_continue_breakpoint(self):
+        self.reverse_continue_breakpoint_internal(async_mode=False)
+
+    @add_test_categories(["dwarf"])
+    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.ReverseContinue()
+        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)
+
+    @add_test_categories(["dwarf"])
+    def test_reverse_continue_skip_breakpoint(self):
+        self.reverse_continue_skip_breakpoint_internal(async_mode=False)
+    
+    @add_test_categories(["dwarf"])
+    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.
+        # 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.ReverseContinue()
+        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/main.c b/lldb/test/API/functionalities/reverse-execution/main.c
new file mode 100644
index 0000000000000..40e45dc9f5c31
--- /dev/null
+++ b/lldb/test/API/functionalities/reverse-execution/main.c
@@ -0,0 +1,14 @@
+volatile int false_condition = 0;
+
+static void start_recording() {}
+
+static void trigger_breakpoint() {}
+
+static void stop_recording() {}
+
+int main() {
+  start_recording();
+  trigger_breakpoint();
+  stop_recording();
+  return 0;
+}
diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp
index 544e9ffb172bf..baf511edccecb 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -907,6 +907,9 @@ llvm::json::Value CreateThreadStopped(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 a91cc6718f4df..2e2047745c131 100644
--- a/lldb/tools/lldb-dap/LLDBUtils.cpp
+++ b/lldb/tools/lldb-dap/LLDBUtils.cpp
@@ -110,6 +110,7 @@ bool ThreadHasStopReason(lldb::SBThread &thread) {
   case lldb::eStopReasonFork:
   case lldb::eStopReasonVFork:
   case lldb::eStopReasonVForkDone:
+  case lldb::eStopReasonHistoryBoundary:
     return true;
   case lldb::eStopReasonThreadExiting:
   case lldb::eStopReasonInvalid:



More information about the lldb-commits mailing list