[Lldb-commits] [lldb] 1408684 - [lldb] Introduce PlatformQemuUser

Pavel Labath via lldb-commits lldb-commits at lists.llvm.org
Tue Nov 30 05:22:30 PST 2021


Author: Pavel Labath
Date: 2021-11-30T14:16:08+01:00
New Revision: 1408684957bbfb5b412e0ef3c027c88daa1058eb

URL: https://github.com/llvm/llvm-project/commit/1408684957bbfb5b412e0ef3c027c88daa1058eb
DIFF: https://github.com/llvm/llvm-project/commit/1408684957bbfb5b412e0ef3c027c88daa1058eb.diff

LOG: [lldb] Introduce PlatformQemuUser

This adds a new platform class, whose job is to enable running
(debugging) executables under qemu.

(For general information about qemu, I recommend reading the RFC thread
on lldb-dev
<https://lists.llvm.org/pipermail/lldb-dev/2021-October/017106.html>.)

This initial patch implements the necessary boilerplate as well as the
minimal amount of functionality needed to actually be able to do
something useful (which, in this case means debugging a fully statically
linked executable).

The knobs necessary to emulate dynamically linked programs, as well as
to control other aspects of qemu operation (the emulated cpu, for
instance) will be added in subsequent patches. Same goes for the ability
to automatically bind to the executables of the emulated architecture.

Currently only two settings are available:
- architecture: the architecture that we should emulate
- emulator-path: the path to the emulator

Even though this patch is relatively small, it doesn't lack subtleties
that are worth calling out explicitly:
- named sockets: qemu supports tcp and unix socket connections, both of
  them in the "forward connect" mode (qemu listening, lldb connecting).
  Forward TCP connections are impossible to realise in a race-free way.
  This is the reason why I chose unix sockets as they have larger, more
  structured names, which can guarantee that there are no collisions
  between concurrent connection attempts.
- the above means that this code will not work on windows. I don't think
  that's an issue since user mode qemu does not support windows anyway.
- Right now, I am leaving the code enabled for windows, but maybe it
  would be better to disable it (otoh, disabling it means windows
  developers can't check they don't break it)
- qemu-user also does not support macOS, so one could contemplate
  disabling it there too. However, macOS does support named sockets, so
  one can even run the (mock) qemu tests there, and I think it'd be a
  shame to lose that.

Differential Revision: https://reviews.llvm.org/D114509

Added: 
    lldb/source/Plugins/Platform/QemuUser/CMakeLists.txt
    lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp
    lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.h
    lldb/source/Plugins/Platform/QemuUser/PlatformQemuUserProperties.td
    lldb/test/API/qemu/Makefile
    lldb/test/API/qemu/TestQemuLaunch.py
    lldb/test/API/qemu/main.c
    lldb/test/API/qemu/qemu.py

Modified: 
    lldb/packages/Python/lldbsuite/test/gdbclientutils.py
    lldb/source/Plugins/Platform/CMakeLists.txt

Removed: 
    


################################################################################
diff  --git a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py
index 6b4650eda0735..78854bb9dae87 100644
--- a/lldb/packages/Python/lldbsuite/test/gdbclientutils.py
+++ b/lldb/packages/Python/lldbsuite/test/gdbclientutils.py
@@ -339,7 +339,7 @@ class UnexpectedPacketException(Exception):
         pass
 
 
-class ServerSocket:
+class ServerChannel:
     """
     A wrapper class for TCP or pty-based server.
     """
@@ -366,22 +366,14 @@ def sendall(self, data):
         """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 @@ def sendall(self, data):
         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 @@ def run(self):
         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
index 7d1e095311cbd..6869587f917eb 100644
--- a/lldb/source/Plugins/Platform/CMakeLists.txt
+++ b/lldb/source/Plugins/Platform/CMakeLists.txt
@@ -6,4 +6,5 @@ add_subdirectory(MacOSX)
 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
index 0000000000000..03a5ba17044fa
--- /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.cpp b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.cpp
new file mode 100644
index 0000000000000..90c290b6fbc79
--- /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<OptionValueProperties>(
+        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<ArchSpec> 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/PlatformQemuUser.h b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUser.h
new file mode 100644
index 0000000000000..f4f5d224a8cdf
--- /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<ArchSpec> 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/PlatformQemuUserProperties.td b/lldb/source/Plugins/Platform/QemuUser/PlatformQemuUserProperties.td
new file mode 100644
index 0000000000000..abfab7f59de40
--- /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
index 0000000000000..10495940055b6
--- /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
index 0000000000000..1dd3dbb764044
--- /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 *
+
+
+ at skipIfRemote
+ at 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
index 0000000000000..243178f03c355
--- /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
index 0000000000000..d35c24dbc43aa
--- /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()


        


More information about the lldb-commits mailing list