diff --git a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py --- a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py +++ b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py @@ -339,7 +339,7 @@ pass -class ServerSocket: +class ServerChannel: """ A wrapper class for TCP or pty-based server. """ @@ -366,22 +366,14 @@ """Send the data to the connected client.""" -class TCPServerSocket(ServerSocket): - def __init__(self): - family, type, proto, _, addr = socket.getaddrinfo( - "localhost", 0, proto=socket.IPPROTO_TCP)[0] +class ServerSocket(ServerChannel): + def __init__(self, family, type, proto, addr): self._server_socket = socket.socket(family, type, proto) self._connection = None self._server_socket.bind(addr) self._server_socket.listen(1) - def get_connect_address(self): - return "[{}]:{}".format(*self._server_socket.getsockname()) - - def get_connect_url(self): - return "connect://" + self.get_connect_address() - def close_server(self): self._server_socket.close() @@ -410,7 +402,31 @@ return self._connection.sendall(data) -class PtyServerSocket(ServerSocket): +class TCPServerSocket(ServerSocket): + def __init__(self): + family, type, proto, _, addr = socket.getaddrinfo( + "localhost", 0, proto=socket.IPPROTO_TCP)[0] + super().__init__(family, type, proto, addr) + + def get_connect_address(self): + return "[{}]:{}".format(*self._server_socket.getsockname()) + + def get_connect_url(self): + return "connect://" + self.get_connect_address() + + +class UnixServerSocket(ServerSocket): + def __init__(self, addr): + super().__init__(socket.AF_UNIX, socket.SOCK_STREAM, 0, addr) + + def get_connect_address(self): + return self._server_socket.getsockname() + + def get_connect_url(self): + return "unix-connect://" + self.get_connect_address() + + +class PtyServerSocket(ServerChannel): def __init__(self): import pty import tty @@ -486,6 +502,7 @@ try: self._socket.accept() except: + traceback.print_exc() return self._shouldSendAck = True self._receivedData = "" diff --git a/lldb/source/Plugins/Platform/CMakeLists.txt b/lldb/source/Plugins/Platform/CMakeLists.txt --- a/lldb/source/Plugins/Platform/CMakeLists.txt +++ b/lldb/source/Plugins/Platform/CMakeLists.txt @@ -6,4 +6,5 @@ add_subdirectory(NetBSD) add_subdirectory(OpenBSD) add_subdirectory(POSIX) +add_subdirectory(QemuUser) add_subdirectory(Windows) diff --git a/lldb/source/Plugins/Platform/QemuUser/CMakeLists.txt b/lldb/source/Plugins/Platform/QemuUser/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Platform/QemuUser/CMakeLists.txt @@ -0,0 +1,20 @@ +lldb_tablegen(PlatformQemuUserProperties.inc -gen-lldb-property-defs + SOURCE PlatformQemuUserProperties.td + TARGET LLDBPluginPlatformQemuUserPropertiesGen) + +lldb_tablegen(PlatformQemuUserPropertiesEnum.inc -gen-lldb-property-enum-defs + SOURCE PlatformQemuUserProperties.td + TARGET LLDBPluginPlatformQemuUserPropertiesEnumGen) + +add_lldb_library(lldbPluginPlatformQemuUser PLUGIN + PlatformQemuUser.cpp + + LINK_LIBS + lldbUtility + LINK_COMPONENTS + Support + ) + +add_dependencies(lldbPluginPlatformQemuUser + LLDBPluginPlatformQemuUserPropertiesGen + LLDBPluginPlatformQemuUserPropertiesEnumGen) diff --git a/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.h b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.h @@ -0,0 +1,57 @@ +//===-- PlatformQemuUser.h ------------------------------------------------===// +// +// 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 "lldb/Host/Host.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Target/Platform.h" + +namespace lldb_private { + +class PlatformQemuUser : public Platform { +public: + static void Initialize(); + static void Terminate(); + + static llvm::StringRef GetPluginNameStatic() { return "qemu-user"; } + static llvm::StringRef GetPluginDescriptionStatic(); + + llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); } + llvm::StringRef GetDescription() override { + return GetPluginDescriptionStatic(); + } + + UserIDResolver &GetUserIDResolver() override { + return HostInfo::GetUserIDResolver(); + } + + std::vector GetSupportedArchitectures() override; + + lldb::ProcessSP DebugProcess(ProcessLaunchInfo &launch_info, + Debugger &debugger, Target &target, + Status &error) override; + + lldb::ProcessSP Attach(ProcessAttachInfo &attach_info, Debugger &debugger, + Target *target, Status &status) override { + status.SetErrorString("Not supported"); + return nullptr; + } + + bool IsConnected() const override { return true; } + + void CalculateTrapHandlerSymbolNames() override {} + + Environment GetEnvironment() override { return Host::GetEnvironment(); } + +private: + static lldb::PlatformSP CreateInstance(bool force, const ArchSpec *arch); + static void DebuggerInitialize(Debugger &debugger); + + PlatformQemuUser() : Platform(/*is_host=*/false) {} +}; + +} // namespace lldb_private diff --git a/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp @@ -0,0 +1,148 @@ +//===-- PlatformQemuUser.cpp ----------------------------------------------===// +// +// 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 "Plugins/Platform/QemuUser/PlatformQemuUser.h" +#include "Plugins/Process/gdb-remote/ProcessGDBRemote.h" +#include "lldb/Core/PluginManager.h" +#include "lldb/Host/FileSystem.h" +#include "lldb/Host/ProcessLaunchInfo.h" +#include "lldb/Interpreter/OptionValueProperties.h" +#include "lldb/Target/Process.h" +#include "lldb/Target/Target.h" +#include "lldb/Utility/Listener.h" +#include "lldb/Utility/Log.h" + +using namespace lldb; +using namespace lldb_private; + +LLDB_PLUGIN_DEFINE(PlatformQemuUser) + +#define LLDB_PROPERTIES_platformqemuuser +#include "PlatformQemuUserProperties.inc" + +enum { +#define LLDB_PROPERTIES_platformqemuuser +#include "PlatformQemuUserPropertiesEnum.inc" +}; + +class PluginProperties : public Properties { +public: + PluginProperties() { + m_collection_sp = std::make_shared( + ConstString(PlatformQemuUser::GetPluginNameStatic())); + m_collection_sp->Initialize(g_platformqemuuser_properties); + } + + llvm::StringRef GetArchitecture() { + return m_collection_sp->GetPropertyAtIndexAsString( + nullptr, ePropertyArchitecture, ""); + } + + FileSpec GetEmulatorPath() { + return m_collection_sp->GetPropertyAtIndexAsFileSpec(nullptr, + ePropertyEmulatorPath); + } +}; + +static PluginProperties &GetGlobalProperties() { + static PluginProperties g_settings; + return g_settings; +} + +llvm::StringRef PlatformQemuUser::GetPluginDescriptionStatic() { + return "Platform for debugging binaries under user mode qemu"; +} + +void PlatformQemuUser::Initialize() { + PluginManager::RegisterPlugin( + GetPluginNameStatic(), GetPluginDescriptionStatic(), + PlatformQemuUser::CreateInstance, PlatformQemuUser::DebuggerInitialize); +} + +void PlatformQemuUser::Terminate() { + PluginManager::UnregisterPlugin(PlatformQemuUser::CreateInstance); +} + +void PlatformQemuUser::DebuggerInitialize(Debugger &debugger) { + if (!PluginManager::GetSettingForPlatformPlugin( + debugger, ConstString(GetPluginNameStatic()))) { + PluginManager::CreateSettingForPlatformPlugin( + debugger, GetGlobalProperties().GetValueProperties(), + ConstString("Properties for the qemu-user platform plugin."), + /*is_global_property=*/true); + } +} + +PlatformSP PlatformQemuUser::CreateInstance(bool force, const ArchSpec *arch) { + if (force) + return PlatformSP(new PlatformQemuUser()); + return nullptr; +} + +std::vector PlatformQemuUser::GetSupportedArchitectures() { + llvm::Triple triple = HostInfo::GetArchitecture().GetTriple(); + triple.setEnvironment(llvm::Triple::UnknownEnvironment); + triple.setArchName(GetGlobalProperties().GetArchitecture()); + if (triple.getArch() != llvm::Triple::UnknownArch) + return {ArchSpec(triple)}; + return {}; +} + +static auto get_arg_range(const Args &args) { + return llvm::make_range(args.GetArgumentArrayRef().begin(), + args.GetArgumentArrayRef().end()); +} + +lldb::ProcessSP PlatformQemuUser::DebugProcess(ProcessLaunchInfo &launch_info, + Debugger &debugger, + Target &target, Status &error) { + Log *log = GetLogIfAnyCategoriesSet(LIBLLDB_LOG_PLATFORM); + + std::string qemu = GetGlobalProperties().GetEmulatorPath().GetPath(); + + llvm::SmallString<0> socket_model, socket_path; + HostInfo::GetProcessTempDir().GetPath(socket_model); + llvm::sys::path::append(socket_model, "qemu-%%%%%%%%.socket"); + do { + llvm::sys::fs::createUniquePath(socket_model, socket_path, false); + } while (FileSystem::Instance().Exists(socket_path)); + + Args args( + {qemu, "-g", socket_path, launch_info.GetExecutableFile().GetPath()}); + for (size_t i = 1; i < launch_info.GetArguments().size(); ++i) + args.AppendArgument(launch_info.GetArguments()[i].ref()); + + LLDB_LOG(log, "{0} -> {1}", get_arg_range(launch_info.GetArguments()), + get_arg_range(args)); + + launch_info.SetArguments(args, true); + launch_info.SetLaunchInSeparateProcessGroup(true); + launch_info.GetFlags().Clear(eLaunchFlagDebug); + launch_info.SetMonitorProcessCallback(ProcessLaunchInfo::NoOpMonitorCallback, + false); + + error = Host::LaunchProcess(launch_info); + if (error.Fail()) + return nullptr; + + ProcessSP process_sp = target.CreateProcess( + launch_info.GetListener(), + process_gdb_remote::ProcessGDBRemote::GetPluginNameStatic(), nullptr, + true); + ListenerSP listener_sp = + Listener::MakeListener("lldb.platform_qemu_user.debugprocess"); + launch_info.SetHijackListener(listener_sp); + Process::ProcessEventHijacker hijacker(*process_sp, listener_sp); + + error = process_sp->ConnectRemote(("unix-connect://" + socket_path).str()); + if (error.Fail()) + return nullptr; + + process_sp->WaitForProcessToStop(llvm::None, nullptr, false, listener_sp); + return process_sp; +} diff --git a/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUserProperties.td b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUserProperties.td new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUserProperties.td @@ -0,0 +1,12 @@ +include "../../../../include/lldb/Core/PropertiesBase.td" + +let Definition = "platformqemuuser" in { + def Architecture: Property<"architecture", "String">, + Global, + DefaultStringValue<"">, + Desc<"Architecture to emulate.">; + def EmulatorPath: Property<"emulator-path", "FileSpec">, + Global, + DefaultStringValue<"">, + Desc<"Path to the emulator binary.">; +} diff --git a/lldb/test/API/qemu/Makefile b/lldb/test/API/qemu/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/qemu/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules diff --git a/lldb/test/API/qemu/TestQemuLaunch.py b/lldb/test/API/qemu/TestQemuLaunch.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/qemu/TestQemuLaunch.py @@ -0,0 +1,83 @@ +from __future__ import print_function +import lldb +import unittest +import os +import json +import stat +import sys +from textwrap import dedent +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +from lldbsuite.test.gdbclientutils import * + + +@skipIfRemote +@skipIfWindows +class TestQemuLaunch(TestBase): + + mydir = TestBase.compute_mydir(__file__) + NO_DEBUG_INFO_TESTCASE = True + + def set_emulator_setting(self, name, value): + self.runCmd("settings set platform.plugin.qemu-user.%s %s" % + (name, value)) + + def setUp(self): + super().setUp() + emulator = self.getBuildArtifact("qemu.py") + with os.fdopen(os.open(emulator, os.O_WRONLY|os.O_CREAT, stat.S_IRWXU), + "w") as e: + + e.write(dedent("""\ + #! {exec!s} + + import runpy + import sys + + sys.path = {path!r} + runpy.run_path({source!r}, run_name='__main__') + """).format(exec=sys.executable, path=sys.path, + source=self.getSourcePath("qemu.py"))) + + self.set_emulator_setting("architecture", self.getArchitecture()) + self.set_emulator_setting("emulator-path", emulator) + + def test_basic_launch(self): + self.build() + exe = self.getBuildArtifact() + + # Create a target using out platform + error = lldb.SBError() + target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error) + self.assertSuccess(error) + self.assertEqual(target.GetPlatform().GetName(), "qemu-user") + + # "Launch" the process. Our fake qemu implementation will pretend it + # immediately exited. + process = target.LaunchSimple( + [self.getBuildArtifact("state.log"), "arg2", "arg3"], None, None) + self.assertIsNotNone(process) + self.assertEqual(process.GetState(), lldb.eStateExited) + self.assertEqual(process.GetExitStatus(), 0x47) + + # Verify the qemu invocation parameters. + with open(self.getBuildArtifact("state.log")) as s: + state = json.load(s) + self.assertEqual(state["program"], self.getBuildArtifact()) + self.assertEqual(state["rest"], ["arg2", "arg3"]) + + def test_bad_emulator_path(self): + self.set_emulator_setting("emulator-path", + self.getBuildArtifact("nonexistent.file")) + + self.build() + exe = self.getBuildArtifact() + + error = lldb.SBError() + target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error) + self.assertEqual(target.GetPlatform().GetName(), "qemu-user") + self.assertSuccess(error) + info = lldb.SBLaunchInfo([]) + target.Launch(info, error) + self.assertTrue(error.Fail()) + self.assertIn("doesn't exist", error.GetCString()) diff --git a/lldb/test/API/qemu/main.c b/lldb/test/API/qemu/main.c new file mode 100644 --- /dev/null +++ b/lldb/test/API/qemu/main.c @@ -0,0 +1,3 @@ +// NB: This code will never be run, but we do need a realistic-looking +// executable for the tests. +int main() {} diff --git a/lldb/test/API/qemu/qemu.py b/lldb/test/API/qemu/qemu.py new file mode 100755 --- /dev/null +++ b/lldb/test/API/qemu/qemu.py @@ -0,0 +1,37 @@ +from textwrap import dedent +import argparse +import socket +import json + +import use_lldb_suite +from lldbsuite.test.gdbclientutils import * + +class MyResponder(MockGDBServerResponder): + def cont(self): + return "W47" + +class FakeEmulator(MockGDBServer): + def __init__(self, addr): + super().__init__(UnixServerSocket(addr)) + self.responder = MyResponder() + +def main(): + parser = argparse.ArgumentParser(description=dedent("""\ + Implements a fake qemu for testing purposes. The executable program + is not actually run. Instead a very basic mock process is presented + to lldb. The emulated program must accept at least one argument. + This should be a path where the emulator will dump its state. This + allows us to check the invocation parameters. + """)) + parser.add_argument('-g', metavar="unix-socket", required=True) + parser.add_argument('program', help="The program to 'emulate'.") + parser.add_argument('state_file', help="Where to dump the emulator state.") + parsed, rest = parser.parse_known_args() + with open(parsed.state_file, "w") as f: + json.dump({"program":parsed.program, "rest":rest}, f) + + emulator = FakeEmulator(parsed.g) + emulator.run() + +if __name__ == "__main__": + main()