Skip to content

[lldb] Implement basic support for reverse-continue #99736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lldb/include/lldb/API/SBProcess.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class LLDB_API SBProcess {
lldb::SBError Destroy();

lldb::SBError Continue();
lldb::SBError Continue(RunDirection direction);

lldb::SBError Stop();

Expand Down
21 changes: 15 additions & 6 deletions lldb/include/lldb/Target/Process.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal APIs, anything not in the lldb namespace can be changed. So this is ok. Though I personally would like to see a:

Status ReverseResume();

I am open to feedback from other here as my mind can easily be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision was to use a direction flag.


/// 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.
///
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lldb/include/lldb/Target/StopInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions lldb/include/lldb/lldb-enumerations.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions lldb/packages/Python/lldbsuite/test/gdbclientutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not obvious when you'd get a stop with no thread. Why did you need this?

Copy link
Contributor Author

@rocallahan rocallahan Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a debugger stop and thread_ does not represent a thread of the inferior. thread_ is the MockGdbServer's Python worker thread, and stop() is the method you call to shut down the server. I'm making this method work in case stop() is called before start() has been called, in the GDBProxyTestBase.tearDown() test scaffolding.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, cool.

self._thread.join()
self._thread = None

def get_connect_address(self):
return self._socket.get_connect_address()
Expand Down
175 changes: 175 additions & 0 deletions lldb/packages/Python/lldbsuite/test/lldbgdbproxy.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading