The main motivation for this was the inconsistency in handling of partial reads/writes between the windows and posix implementations (windows was returning partial reads, posix was trying to fill the buffer completely). I settle on the windows implementation, as that's the more common behavior, and the "eager" version can be implemented on top of that (in most cases, it isn't necessary, since we're writing just a single byte). Since this also required auditing the callers to make sure they're handling partial reads/writes correctly, I used the opportunity to modernize the function signatures as a forcing function. They now use the `Timeout` class (basically an `optional<duration>`) to support both polls (timeout=0) and blocking (timeout=nullopt) operations in a single function, and use an `Expected` instead of a by-ref result to return the number of bytes read/written. As a drive-by, I also fix a problem with the windows implementation where we were rounding the timeout value down, which meant that calls could time out slightly sooner than expected.
496 lines
16 KiB
C++
496 lines
16 KiB
C++
//===-- lldb-gdbserver.cpp --------------------------------------*- C++ -*-===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#ifndef _WIN32
|
|
#include <csignal>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "LLDBServerUtilities.h"
|
|
#include "Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.h"
|
|
#include "Plugins/Process/gdb-remote/ProcessGDBRemoteLog.h"
|
|
#include "lldb/Host/Config.h"
|
|
#include "lldb/Host/ConnectionFileDescriptor.h"
|
|
#include "lldb/Host/FileSystem.h"
|
|
#include "lldb/Host/Pipe.h"
|
|
#include "lldb/Host/common/NativeProcessProtocol.h"
|
|
#include "lldb/Host/common/TCPSocket.h"
|
|
#include "lldb/Target/Process.h"
|
|
#include "lldb/Utility/LLDBLog.h"
|
|
#include "lldb/Utility/Status.h"
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "llvm/Option/ArgList.h"
|
|
#include "llvm/Option/OptTable.h"
|
|
#include "llvm/Option/Option.h"
|
|
#include "llvm/Support/Errno.h"
|
|
#include "llvm/Support/Error.h"
|
|
#include "llvm/Support/WithColor.h"
|
|
|
|
#if defined(__linux__)
|
|
#include "Plugins/Process/Linux/NativeProcessLinux.h"
|
|
#elif defined(__FreeBSD__)
|
|
#include "Plugins/Process/FreeBSD/NativeProcessFreeBSD.h"
|
|
#elif defined(__NetBSD__)
|
|
#include "Plugins/Process/NetBSD/NativeProcessNetBSD.h"
|
|
#elif defined(_WIN32)
|
|
#include "Plugins/Process/Windows/Common/NativeProcessWindows.h"
|
|
#endif
|
|
|
|
#ifndef LLGS_PROGRAM_NAME
|
|
#define LLGS_PROGRAM_NAME "lldb-server"
|
|
#endif
|
|
|
|
#ifndef LLGS_VERSION_STR
|
|
#define LLGS_VERSION_STR "local_build"
|
|
#endif
|
|
|
|
using namespace llvm;
|
|
using namespace lldb;
|
|
using namespace lldb_private;
|
|
using namespace lldb_private::lldb_server;
|
|
using namespace lldb_private::process_gdb_remote;
|
|
|
|
namespace {
|
|
#if defined(__linux__)
|
|
typedef process_linux::NativeProcessLinux::Manager NativeProcessManager;
|
|
#elif defined(__FreeBSD__)
|
|
typedef process_freebsd::NativeProcessFreeBSD::Manager NativeProcessManager;
|
|
#elif defined(__NetBSD__)
|
|
typedef process_netbsd::NativeProcessNetBSD::Manager NativeProcessManager;
|
|
#elif defined(_WIN32)
|
|
typedef NativeProcessWindows::Manager NativeProcessManager;
|
|
#else
|
|
// Dummy implementation to make sure the code compiles
|
|
class NativeProcessManager : public NativeProcessProtocol::Manager {
|
|
public:
|
|
NativeProcessManager(MainLoop &mainloop)
|
|
: NativeProcessProtocol::Manager(mainloop) {}
|
|
|
|
llvm::Expected<std::unique_ptr<NativeProcessProtocol>>
|
|
Launch(ProcessLaunchInfo &launch_info,
|
|
NativeProcessProtocol::NativeDelegate &native_delegate) override {
|
|
llvm_unreachable("Not implemented");
|
|
}
|
|
llvm::Expected<std::unique_ptr<NativeProcessProtocol>>
|
|
Attach(lldb::pid_t pid,
|
|
NativeProcessProtocol::NativeDelegate &native_delegate) override {
|
|
llvm_unreachable("Not implemented");
|
|
}
|
|
};
|
|
#endif
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
// Watch for signals
|
|
static int g_sighup_received_count = 0;
|
|
|
|
static void sighup_handler(MainLoopBase &mainloop) {
|
|
++g_sighup_received_count;
|
|
|
|
Log *log = GetLog(LLDBLog::Process);
|
|
LLDB_LOGF(log, "lldb-server:%s swallowing SIGHUP (receive count=%d)",
|
|
__FUNCTION__, g_sighup_received_count);
|
|
|
|
if (g_sighup_received_count >= 2)
|
|
mainloop.RequestTermination();
|
|
}
|
|
#endif // #ifndef _WIN32
|
|
|
|
void handle_attach_to_pid(GDBRemoteCommunicationServerLLGS &gdb_server,
|
|
lldb::pid_t pid) {
|
|
Status error = gdb_server.AttachToProcess(pid);
|
|
if (error.Fail()) {
|
|
fprintf(stderr, "error: failed to attach to pid %" PRIu64 ": %s\n", pid,
|
|
error.AsCString());
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
void handle_attach_to_process_name(GDBRemoteCommunicationServerLLGS &gdb_server,
|
|
const std::string &process_name) {
|
|
// FIXME implement.
|
|
}
|
|
|
|
void handle_attach(GDBRemoteCommunicationServerLLGS &gdb_server,
|
|
const std::string &attach_target) {
|
|
assert(!attach_target.empty() && "attach_target cannot be empty");
|
|
|
|
// First check if the attach_target is convertible to a long. If so, we'll use
|
|
// it as a pid.
|
|
char *end_p = nullptr;
|
|
const long int pid = strtol(attach_target.c_str(), &end_p, 10);
|
|
|
|
// We'll call it a match if the entire argument is consumed.
|
|
if (end_p &&
|
|
static_cast<size_t>(end_p - attach_target.c_str()) ==
|
|
attach_target.size())
|
|
handle_attach_to_pid(gdb_server, static_cast<lldb::pid_t>(pid));
|
|
else
|
|
handle_attach_to_process_name(gdb_server, attach_target);
|
|
}
|
|
|
|
void handle_launch(GDBRemoteCommunicationServerLLGS &gdb_server,
|
|
llvm::ArrayRef<llvm::StringRef> Arguments) {
|
|
ProcessLaunchInfo info;
|
|
info.GetFlags().Set(eLaunchFlagStopAtEntry | eLaunchFlagDebug |
|
|
eLaunchFlagDisableASLR);
|
|
info.SetArguments(Args(Arguments), true);
|
|
|
|
llvm::SmallString<64> cwd;
|
|
if (std::error_code ec = llvm::sys::fs::current_path(cwd)) {
|
|
llvm::errs() << "Error getting current directory: " << ec.message() << "\n";
|
|
exit(1);
|
|
}
|
|
FileSpec cwd_spec(cwd);
|
|
FileSystem::Instance().Resolve(cwd_spec);
|
|
info.SetWorkingDirectory(cwd_spec);
|
|
info.GetEnvironment() = Host::GetEnvironment();
|
|
|
|
gdb_server.SetLaunchInfo(info);
|
|
|
|
Status error = gdb_server.LaunchProcess();
|
|
if (error.Fail()) {
|
|
llvm::errs() << llvm::formatv("error: failed to launch '{0}': {1}\n",
|
|
Arguments[0], error);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
static Status writeSocketIdToPipe(Pipe &port_pipe,
|
|
const std::string &socket_id) {
|
|
// NB: Include the nul character at the end.
|
|
llvm::StringRef buf(socket_id.data(), socket_id.size() + 1);
|
|
while (!buf.empty()) {
|
|
if (llvm::Expected<size_t> written =
|
|
port_pipe.Write(buf.data(), buf.size()))
|
|
buf = buf.drop_front(*written);
|
|
else
|
|
return Status::FromError(written.takeError());
|
|
}
|
|
return Status();
|
|
}
|
|
|
|
Status writeSocketIdToPipe(const char *const named_pipe_path,
|
|
llvm::StringRef socket_id) {
|
|
Pipe port_name_pipe;
|
|
// Wait for 10 seconds for pipe to be opened.
|
|
if (llvm::Error err = port_name_pipe.OpenAsWriter(named_pipe_path, false,
|
|
std::chrono::seconds{10}))
|
|
return Status::FromError(std::move(err));
|
|
|
|
return writeSocketIdToPipe(port_name_pipe, socket_id.str());
|
|
}
|
|
|
|
Status writeSocketIdToPipe(lldb::pipe_t unnamed_pipe,
|
|
llvm::StringRef socket_id) {
|
|
Pipe port_pipe{LLDB_INVALID_PIPE, unnamed_pipe};
|
|
return writeSocketIdToPipe(port_pipe, socket_id.str());
|
|
}
|
|
|
|
void ConnectToRemote(MainLoop &mainloop,
|
|
GDBRemoteCommunicationServerLLGS &gdb_server,
|
|
bool reverse_connect, llvm::StringRef host_and_port,
|
|
const char *const progname, const char *const subcommand,
|
|
const char *const named_pipe_path, pipe_t unnamed_pipe,
|
|
shared_fd_t connection_fd) {
|
|
Status error;
|
|
|
|
std::unique_ptr<Connection> connection_up;
|
|
std::string url;
|
|
|
|
if (connection_fd != SharedSocket::kInvalidFD) {
|
|
#ifdef _WIN32
|
|
NativeSocket sockfd;
|
|
error = SharedSocket::GetNativeSocket(connection_fd, sockfd);
|
|
if (error.Fail()) {
|
|
llvm::errs() << llvm::formatv("error: GetNativeSocket failed: {0}\n",
|
|
error.AsCString());
|
|
exit(-1);
|
|
}
|
|
connection_up = std::unique_ptr<Connection>(new ConnectionFileDescriptor(
|
|
new TCPSocket(sockfd, /*should_close=*/true)));
|
|
#else
|
|
url = llvm::formatv("fd://{0}", connection_fd).str();
|
|
|
|
// Create the connection.
|
|
::fcntl(connection_fd, F_SETFD, FD_CLOEXEC);
|
|
#endif
|
|
} else if (!host_and_port.empty()) {
|
|
llvm::Expected<std::string> url_exp =
|
|
LLGSArgToURL(host_and_port, reverse_connect);
|
|
if (!url_exp) {
|
|
llvm::errs() << llvm::formatv("error: invalid host:port or URL '{0}': "
|
|
"{1}\n",
|
|
host_and_port,
|
|
llvm::toString(url_exp.takeError()));
|
|
exit(-1);
|
|
}
|
|
|
|
url = std::move(url_exp.get());
|
|
}
|
|
|
|
if (!url.empty()) {
|
|
// Create the connection or server.
|
|
std::unique_ptr<ConnectionFileDescriptor> conn_fd_up{
|
|
new ConnectionFileDescriptor};
|
|
auto connection_result = conn_fd_up->Connect(
|
|
url,
|
|
[named_pipe_path, unnamed_pipe](llvm::StringRef socket_id) {
|
|
// If we have a named pipe to write the socket id back to, do that
|
|
// now.
|
|
if (named_pipe_path && named_pipe_path[0]) {
|
|
Status error = writeSocketIdToPipe(named_pipe_path, socket_id);
|
|
if (error.Fail())
|
|
llvm::errs() << llvm::formatv(
|
|
"failed to write to the named pipe '{0}': {1}\n",
|
|
named_pipe_path, error.AsCString());
|
|
}
|
|
// If we have an unnamed pipe to write the socket id back to, do
|
|
// that now.
|
|
else if (unnamed_pipe != LLDB_INVALID_PIPE) {
|
|
Status error = writeSocketIdToPipe(unnamed_pipe, socket_id);
|
|
if (error.Fail())
|
|
llvm::errs() << llvm::formatv(
|
|
"failed to write to the unnamed pipe: {0}\n", error);
|
|
}
|
|
},
|
|
&error);
|
|
|
|
if (error.Fail()) {
|
|
llvm::errs() << llvm::formatv(
|
|
"error: failed to connect to client at '{0}': {1}\n", url, error);
|
|
exit(-1);
|
|
}
|
|
if (connection_result != eConnectionStatusSuccess) {
|
|
llvm::errs() << llvm::formatv(
|
|
"error: failed to connect to client at '{0}' "
|
|
"(connection status: {1})\n",
|
|
url, static_cast<int>(connection_result));
|
|
exit(-1);
|
|
}
|
|
connection_up = std::move(conn_fd_up);
|
|
}
|
|
error = gdb_server.InitializeConnection(std::move(connection_up));
|
|
if (error.Fail()) {
|
|
llvm::errs() << llvm::formatv("failed to initialize connection\n", error);
|
|
exit(-1);
|
|
}
|
|
llvm::outs() << "Connection established.\n";
|
|
}
|
|
|
|
namespace {
|
|
using namespace llvm::opt;
|
|
|
|
enum ID {
|
|
OPT_INVALID = 0, // This is not an option ID.
|
|
#define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__),
|
|
#include "LLGSOptions.inc"
|
|
#undef OPTION
|
|
};
|
|
|
|
#define OPTTABLE_STR_TABLE_CODE
|
|
#include "LLGSOptions.inc"
|
|
#undef OPTTABLE_STR_TABLE_CODE
|
|
|
|
#define OPTTABLE_PREFIXES_TABLE_CODE
|
|
#include "LLGSOptions.inc"
|
|
#undef OPTTABLE_PREFIXES_TABLE_CODE
|
|
|
|
static constexpr opt::OptTable::Info InfoTable[] = {
|
|
#define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__),
|
|
#include "LLGSOptions.inc"
|
|
#undef OPTION
|
|
};
|
|
|
|
class LLGSOptTable : public opt::GenericOptTable {
|
|
public:
|
|
LLGSOptTable()
|
|
: opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable) {}
|
|
|
|
void PrintHelp(llvm::StringRef Name) {
|
|
std::string Usage =
|
|
(Name + " [options] [[host]:port] [[--] program args...]").str();
|
|
OptTable::printHelp(llvm::outs(), Usage.c_str(), "lldb-server");
|
|
llvm::outs() << R"(
|
|
DESCRIPTION
|
|
lldb-server connects to the LLDB client, which drives the debugging session.
|
|
If no connection options are given, the [host]:port argument must be present
|
|
and will denote the address that lldb-server will listen on. [host] defaults
|
|
to "localhost" if empty. Port can be zero, in which case the port number will
|
|
be chosen dynamically and written to destinations given by --named-pipe and
|
|
--pipe arguments.
|
|
|
|
If no target is selected at startup, lldb-server can be directed by the LLDB
|
|
client to launch or attach to a process.
|
|
|
|
)";
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
int main_gdbserver(int argc, char *argv[]) {
|
|
Status error;
|
|
MainLoop mainloop;
|
|
#ifndef _WIN32
|
|
// Setup signal handlers first thing.
|
|
signal(SIGPIPE, SIG_IGN);
|
|
MainLoop::SignalHandleUP sighup_handle =
|
|
mainloop.RegisterSignal(SIGHUP, sighup_handler, error);
|
|
#endif
|
|
|
|
const char *progname = argv[0];
|
|
const char *subcommand = argv[1];
|
|
std::string attach_target;
|
|
std::string named_pipe_path;
|
|
std::string log_file;
|
|
StringRef
|
|
log_channels; // e.g. "lldb process threads:gdb-remote default:linux all"
|
|
lldb::pipe_t unnamed_pipe = LLDB_INVALID_PIPE;
|
|
bool reverse_connect = false;
|
|
shared_fd_t connection_fd = SharedSocket::kInvalidFD;
|
|
|
|
// ProcessLaunchInfo launch_info;
|
|
ProcessAttachInfo attach_info;
|
|
|
|
LLGSOptTable Opts;
|
|
llvm::BumpPtrAllocator Alloc;
|
|
llvm::StringSaver Saver(Alloc);
|
|
bool HasError = false;
|
|
opt::InputArgList Args = Opts.parseArgs(argc - 1, argv + 1, OPT_UNKNOWN,
|
|
Saver, [&](llvm::StringRef Msg) {
|
|
WithColor::error() << Msg << "\n";
|
|
HasError = true;
|
|
});
|
|
std::string Name =
|
|
(llvm::sys::path::filename(argv[0]) + " g[dbserver]").str();
|
|
std::string HelpText =
|
|
"Use '" + Name + " --help' for a complete list of options.\n";
|
|
if (HasError) {
|
|
llvm::errs() << HelpText;
|
|
return 1;
|
|
}
|
|
|
|
if (Args.hasArg(OPT_help)) {
|
|
Opts.PrintHelp(Name);
|
|
return 0;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
if (Args.hasArg(OPT_setsid)) {
|
|
// Put llgs into a new session. Terminals group processes
|
|
// into sessions and when a special terminal key sequences
|
|
// (like control+c) are typed they can cause signals to go out to
|
|
// all processes in a session. Using this --setsid (-S) option
|
|
// will cause debugserver to run in its own sessions and be free
|
|
// from such issues.
|
|
//
|
|
// This is useful when llgs is spawned from a command
|
|
// line application that uses llgs to do the debugging,
|
|
// yet that application doesn't want llgs receiving the
|
|
// signals sent to the session (i.e. dying when anyone hits ^C).
|
|
{
|
|
const ::pid_t new_sid = setsid();
|
|
if (new_sid == -1) {
|
|
WithColor::warning()
|
|
<< llvm::formatv("failed to set new session id for {0} ({1})\n",
|
|
LLGS_PROGRAM_NAME, llvm::sys::StrError());
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
log_file = Args.getLastArgValue(OPT_log_file).str();
|
|
log_channels = Args.getLastArgValue(OPT_log_channels);
|
|
named_pipe_path = Args.getLastArgValue(OPT_named_pipe).str();
|
|
reverse_connect = Args.hasArg(OPT_reverse_connect);
|
|
attach_target = Args.getLastArgValue(OPT_attach).str();
|
|
if (Args.hasArg(OPT_pipe)) {
|
|
uint64_t Arg;
|
|
if (!llvm::to_integer(Args.getLastArgValue(OPT_pipe), Arg)) {
|
|
WithColor::error() << "invalid '--pipe' argument\n" << HelpText;
|
|
return 1;
|
|
}
|
|
unnamed_pipe = (pipe_t)Arg;
|
|
}
|
|
if (Args.hasArg(OPT_fd)) {
|
|
int64_t fd;
|
|
if (!llvm::to_integer(Args.getLastArgValue(OPT_fd), fd)) {
|
|
WithColor::error() << "invalid '--fd' argument\n" << HelpText;
|
|
return 1;
|
|
}
|
|
connection_fd = (shared_fd_t)fd;
|
|
}
|
|
|
|
if (!LLDBServerUtilities::SetupLogging(
|
|
log_file, log_channels,
|
|
LLDB_LOG_OPTION_PREPEND_TIMESTAMP |
|
|
LLDB_LOG_OPTION_PREPEND_FILE_FUNCTION))
|
|
return -1;
|
|
|
|
std::vector<llvm::StringRef> Inputs;
|
|
for (opt::Arg *Arg : Args.filtered(OPT_INPUT))
|
|
Inputs.push_back(Arg->getValue());
|
|
if (opt::Arg *Arg = Args.getLastArg(OPT_REM)) {
|
|
for (const char *Val : Arg->getValues())
|
|
Inputs.push_back(Val);
|
|
}
|
|
if (Inputs.empty() && connection_fd == SharedSocket::kInvalidFD) {
|
|
WithColor::error() << "no connection arguments\n" << HelpText;
|
|
return 1;
|
|
}
|
|
|
|
NativeProcessManager manager(mainloop);
|
|
GDBRemoteCommunicationServerLLGS gdb_server(mainloop, manager);
|
|
|
|
llvm::StringRef host_and_port;
|
|
if (!Inputs.empty() && connection_fd == SharedSocket::kInvalidFD) {
|
|
host_and_port = Inputs.front();
|
|
Inputs.erase(Inputs.begin());
|
|
}
|
|
|
|
// Any arguments left over are for the program that we need to launch. If
|
|
// there
|
|
// are no arguments, then the GDB server will start up and wait for an 'A'
|
|
// packet
|
|
// to launch a program, or a vAttach packet to attach to an existing process,
|
|
// unless
|
|
// explicitly asked to attach with the --attach={pid|program_name} form.
|
|
if (!attach_target.empty())
|
|
handle_attach(gdb_server, attach_target);
|
|
else if (!Inputs.empty())
|
|
handle_launch(gdb_server, Inputs);
|
|
|
|
// Print version info.
|
|
printf("%s-%s\n", LLGS_PROGRAM_NAME, LLGS_VERSION_STR);
|
|
|
|
ConnectToRemote(mainloop, gdb_server, reverse_connect, host_and_port,
|
|
progname, subcommand, named_pipe_path.c_str(),
|
|
unnamed_pipe, connection_fd);
|
|
|
|
if (!gdb_server.IsConnected()) {
|
|
fprintf(stderr, "no connection information provided, unable to run\n");
|
|
return 1;
|
|
}
|
|
|
|
Status ret = mainloop.Run();
|
|
if (ret.Fail()) {
|
|
fprintf(stderr, "lldb-server terminating due to error: %s\n",
|
|
ret.AsCString());
|
|
return 1;
|
|
}
|
|
fprintf(stderr, "lldb-server exiting...\n");
|
|
|
|
return 0;
|
|
}
|