text: /* $Id: DBGFR3PlugIn.cpp $ */
  /** @file
   * DBGF - Debugger Facility, Plug-In Support.
   */
  
  /*
   * Copyright (C) 2008-2017 Oracle Corporation
   *
   * This file is part of VirtualBox Open Source Edition (OSE), as
   * available from http://www.virtualbox.org. This file is free software;
   * you can redistribute it and/or modify it under the terms of the GNU
   * General Public License (GPL) as published by the Free Software
   * Foundation, in version 2 as it comes in the "COPYING" file of the
   * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
   * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
   */
  
  
  /*********************************************************************************************************************************
  *   Header Files                                                                                                                 *
  *********************************************************************************************************************************/
  #define LOG_GROUP LOG_GROUP_DBGF
  #include <VBox/vmm/dbgf.h>
  #include <VBox/vmm/mm.h>
  #include "DBGFInternal.h"
  #include <VBox/vmm/uvm.h>
  #include <VBox/vmm/vm.h>
  #include <VBox/err.h>
  #include <VBox/log.h>
  #include <VBox/version.h>
  
  #include <iprt/alloca.h>
  #include <iprt/assert.h>
  #include <iprt/ctype.h>
  #include <iprt/env.h>
  #include <iprt/dir.h>
  #include <iprt/ldr.h>
  #include <iprt/param.h>
  #include <iprt/path.h>
  
  
  /*********************************************************************************************************************************
  *   Defined Constants And Macros                                                                                                 *
  *********************************************************************************************************************************/
  
  #define DBGF_PLUG_IN_READ_LOCK(pUVM) \
      do { int rcLock = RTCritSectRwEnterShared(&pUVM->dbgf.s.CritSect); AssertRC(rcLock); } while (0)
  #define DBGF_PLUG_IN_READ_UNLOCK(pUVM) \
      do { int rcLock = RTCritSectRwLeaveShared(&pUVM->dbgf.s.CritSect); AssertRC(rcLock); } while (0)
  
  #define DBGF_PLUG_IN_WRITE_LOCK(pUVM) \
      do { int rcLock = RTCritSectRwEnterExcl(&pUVM->dbgf.s.CritSect); AssertRC(rcLock); } while (0)
  #define DBGF_PLUG_IN_WRITE_UNLOCK(pUVM) \
      do { int rcLock = RTCritSectRwLeaveExcl(&pUVM->dbgf.s.CritSect); AssertRC(rcLock); } while (0)
  
  /** Max allowed length of a plug-in name (excludes the path and suffix). */
  #define DBGFPLUGIN_MAX_NAME     64
  
  
  /*********************************************************************************************************************************
  *   Structures and Typedefs                                                                                                      *
  *********************************************************************************************************************************/
  /**
   * Plug-in tracking record.
   */
  typedef struct DBGFPLUGIN
  {
      /** Pointer to the next plug-in. */
      struct DBGFPLUGIN  *pNext;
      /** The loader handle.  */
      RTLDRMOD            hLdrMod;
      /** The plug-in entry point. */
      PFNDBGFPLUGIN       pfnEntry;
      /** The name length. */
      uint8_t             cchName;
      /** The plug-in name (variable length).  */
      char                szName[1];
  } DBGFPLUGIN;
  /** Pointer to plug-in tracking record. */
  typedef DBGFPLUGIN *PDBGFPLUGIN;
  
  
  /*********************************************************************************************************************************
  *   Internal Functions                                                                                                           *
  *********************************************************************************************************************************/
  static DECLCALLBACK(void) dbgfPlugInUnloadAll(PUVM pUVM);
  static FNDBGFHANDLERINT dbgfR3PlugInInfoList;
  
  
  /**
   * Internal init routine called by DBGFR3Init().
   *
   * @returns VBox status code.
   * @param   pUVM    The user mode VM handle.
   */
  int dbgfR3PlugInInit(PUVM pUVM)
  {
      return DBGFR3InfoRegisterInternal(pUVM->pVM, "plugins", "Lists the debugger plug-ins.", dbgfR3PlugInInfoList);
  }
  
  
  /**
   * Internal cleanup routine called by DBGFR3Term().
   *
   * @param   pUVM    The user mode VM handle.
   */
  void dbgfR3PlugInTerm(PUVM pUVM)
  {
      dbgfPlugInUnloadAll(pUVM);
  }
  
  
  /**
   * Extracts the plug-in name from a plug-in specifier that may or may not
   * include path and/or suffix.
   *
   * @returns VBox status code.
   *
   * @param   pszDst      Where to return the name. At least DBGFPLUGIN_MAX_NAME
   *                      worth of buffer space.
   * @param   pszPlugIn   The plug-in module specifier to parse.
   * @param   pErrInfo    Optional error information structure.
   */
  static int dbgfPlugInExtractName(char *pszDst, const char *pszPlugIn, PRTERRINFO pErrInfo)
  {
      /*
       * Parse out the name stopping at the extension.
       */
      const char *pszName = RTPathFilename(pszPlugIn);
      if (!pszName || !*pszName)
          return VERR_INVALID_NAME;
      if (!RTStrNICmp(pszName, RT_STR_TUPLE(DBGF_PLUG_IN_PREFIX)))
      {
          pszName += sizeof(DBGF_PLUG_IN_PREFIX) - 1;
          if (!*pszName)
              return RTErrInfoSetF(pErrInfo, VERR_INVALID_NAME, "Invalid plug-in name: nothing after the prefix");
      }
  
      int     ch;
      size_t  cchName = 0;
      while (   (ch = pszName[cchName]) != '\0'
             && ch != '.')
      {
          if (   RT_C_IS_ALPHA(ch)
              || (RT_C_IS_DIGIT(ch) && cchName != 0))
              cchName++;
          else
          {
              if (!RT_C_IS_DIGIT(ch))
                  return RTErrInfoSetF(pErrInfo, VERR_INVALID_NAME, "Invalid plug-in name: '%c' is not alphanumeric", ch);
              return RTErrInfoSetF(pErrInfo, VERR_INVALID_NAME,
                                   "Invalid plug-in name: Cannot start with a digit (after the prefix)");
          }
      }
  
      if (cchName >= DBGFPLUGIN_MAX_NAME)
          return RTErrInfoSetF(pErrInfo, VERR_INVALID_NAME, "Invalid plug-in name: too long (max %u)", DBGFPLUGIN_MAX_NAME);
  
      /*
       * We're very picky about the extension when present.
       */
      if (   ch == '.'
          && RTStrICmp(&pszName[cchName], RTLdrGetSuff()))
          return RTErrInfoSetF(pErrInfo, VERR_INVALID_NAME,
                               "Invalid plug-in name: Suffix isn't the default dll/so/dylib one (%s): '%s'",
                               RTLdrGetSuff(), &pszName[cchName]);
  
      /*
       * Copy it.
       */
      memcpy(pszDst, pszName, cchName);
      pszDst[cchName] = '\0';
      return VINF_SUCCESS;
  }
  
  
  /**
   * Locate a loaded plug-in.
   *
   * @returns Pointer to the plug-in tracking structure.
   * @param   pUVM                Pointer to the user-mode VM structure.
   * @param   pszName             The name of the plug-in we're looking for.
   * @param   ppPrev              Where to optionally return the pointer to the
   *                              previous list member.
   */
  static PDBGFPLUGIN dbgfR3PlugInLocate(PUVM pUVM, const char *pszName, PDBGFPLUGIN *ppPrev)
  {
      PDBGFPLUGIN pPrev = NULL;
      PDBGFPLUGIN pCur  = pUVM->dbgf.s.pPlugInHead;
      while (pCur)
      {
          if (!RTStrICmp(pCur->szName, pszName))
          {
              if (ppPrev)
                  *ppPrev = pPrev;
              return pCur;
          }
  
          /* advance */
          pPrev = pCur;
          pCur  = pCur->pNext;
      }
      return NULL;
  }
  
  
  /**
   * Try load the specified plug-in module.
   *
   * @returns VINF_SUCCESS on success, path error or loader error on failure.
   *
   * @param   pPlugIn     The plug-in tracing record.
   * @param   pszModule   Module name.
   * @param   pErrInfo    Optional error information structure.
   */
  static int dbgfR3PlugInTryLoad(PDBGFPLUGIN pPlugIn, const char *pszModule, PRTERRINFO pErrInfo)
  {
      /*
       * Load it and try resolve the entry point.
       */
      int rc = SUPR3HardenedVerifyPlugIn(pszModule, pErrInfo);
      if (RT_SUCCESS(rc))
          rc = RTLdrLoadEx(pszModule, &pPlugIn->hLdrMod, RTLDRLOAD_FLAGS_LOCAL, pErrInfo);
      if (RT_SUCCESS(rc))
      {
          rc = RTLdrGetSymbol(pPlugIn->hLdrMod, DBGF_PLUG_IN_ENTRYPOINT, (void **)&pPlugIn->pfnEntry);
          if (RT_SUCCESS(rc))
          {
              LogRel(("DBGF: Loaded Plug-In '%s' (%s)\n", pPlugIn->szName, pszModule));
              return VINF_SUCCESS;
          }
  
          RTErrInfoSet(pErrInfo, rc, "Failed to locate plug-in entrypoint (" DBGF_PLUG_IN_ENTRYPOINT ")" );
          LogRel(("DBGF: RTLdrGetSymbol('%s', '%s',) -> %Rrc\n", pszModule, DBGF_PLUG_IN_ENTRYPOINT, rc));
  
          RTLdrClose(pPlugIn->hLdrMod);
          pPlugIn->hLdrMod = NIL_RTLDRMOD;
      }
      return rc;
  }
  
  
  /**
   * RTPathTraverseList callback.
   *
   * @returns See FNRTPATHTRAVERSER.
   *
   * @param   pchPath     See FNRTPATHTRAVERSER.
   * @param   cchPath     See FNRTPATHTRAVERSER.
   * @param   pvUser1     The plug-in specifier.
   * @param   pvUser2     The plug-in tracking record.
   */
  static DECLCALLBACK(int) dbgfR3PlugInLoadCallback(const char *pchPath, size_t cchPath, void *pvUser1, void *pvUser2)
  {
      PDBGFPLUGIN pPlugIn   = (PDBGFPLUGIN)pvUser1;
      PRTERRINFO  pErrInfo  = (PRTERRINFO)pvUser2;
  
      /*
       * Join the path and the specified plug-in name, adding prefix and suffix.
       */
      const char  *pszSuff   = RTLdrGetSuff();
      size_t const cchSuff   = strlen(pszSuff);
      size_t const cchModule = cchPath + sizeof(RTPATH_SLASH_STR) + sizeof(DBGF_PLUG_IN_PREFIX) + pPlugIn->cchName + cchSuff + 4;
      char        *pszModule = (char *)alloca(cchModule);
      AssertReturn(pszModule, VERR_TRY_AGAIN);
      memcpy(pszModule, pchPath, cchPath);
      pszModule[cchPath] = '\0';
  
      int rc = RTPathAppend(pszModule, cchModule, DBGF_PLUG_IN_PREFIX);
      AssertRCReturn(rc, VERR_TRY_AGAIN);
      strcat(&pszModule[cchPath], pPlugIn->szName);
      strcat(&pszModule[cchPath + sizeof(DBGF_PLUG_IN_PREFIX) - 1 + pPlugIn->cchName], pszSuff);
      Assert(strlen(pszModule) < cchModule - 4);
  
      if (RTPathExists(pszModule))
      {
          rc = dbgfR3PlugInTryLoad(pPlugIn, pszModule, pErrInfo);
          if (RT_SUCCESS(rc))
              return VINF_SUCCESS;
      }
  
      return VERR_TRY_AGAIN;
  }
  
  
  /**
   * Loads a plug-in.
   *
   * @returns VBox status code.
   * @param   pUVM                Pointer to the user-mode VM structure.
   * @param   pszName             The plug-in name.
   * @param   pszMaybeModule      Path to the plug-in, or just the
   *                              plug-in name as specified by the user.  Ignored
   *                              if no path.
   * @param   pErrInfo            Optional error information structure.
   */
  static DECLCALLBACK(int) dbgfR3PlugInLoad(PUVM pUVM, const char *pszName, const char *pszMaybeModule, PRTERRINFO pErrInfo)
  {
      DBGF_PLUG_IN_WRITE_LOCK(pUVM);
  
      /*
       * Check if a plug-in by the given name already exists.
       */
      PDBGFPLUGIN pPlugIn = dbgfR3PlugInLocate(pUVM, pszName, NULL);
      if (pPlugIn)
      {
          DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
          return RTErrInfoSetF(pErrInfo, VERR_ALREADY_EXISTS, "A plug-in by the name '%s' already exists", pszName);
      }
  
      /*
       * Create a module structure and we can pass around via RTPathTraverseList if needed.
       */
      size_t cbName = strlen(pszName) + 1;
      pPlugIn = (PDBGFPLUGIN)MMR3HeapAllocZU(pUVM, MM_TAG_DBGF, RT_UOFFSETOF_DYN(DBGFPLUGIN, szName[cbName]));
      if (RT_UNLIKELY(!pPlugIn))
      {
          DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
          return VERR_NO_MEMORY;
      }
      memcpy(pPlugIn->szName, pszName, cbName);
      pPlugIn->cchName = (uint8_t)cbName - 1;
      Assert(pPlugIn->cchName == cbName - 1);
  
      /*
       * If the caller specified a path, try load exactly what was specified.
       */
      int rc;
      if (RTPathHavePath(pszMaybeModule))
          rc = dbgfR3PlugInTryLoad(pPlugIn, pszMaybeModule, pErrInfo);
      else
      {
          /*
           * No path specified, search for the plug-in using the canonical
           * module name for it.
           */
          RTErrInfoClear(pErrInfo);
  
          /* 1. The private architecture directory. */
          char szPath[_4K];
          rc = RTPathAppPrivateArch(szPath, sizeof(szPath));
          if (RT_SUCCESS(rc))
              rc = RTPathTraverseList(szPath, '\0', dbgfR3PlugInLoadCallback, pPlugIn, pErrInfo);
          if (RT_FAILURE_NP(rc))
          {
              /* 2. The config value 'PlugInPath' */
              int rc2 = CFGMR3QueryString(CFGMR3GetChild(CFGMR3GetRootU(pUVM), "/DBGF"), "PlugInPath", szPath, sizeof(szPath));
              if (RT_SUCCESS(rc2))
                  rc = RTPathTraverseList(szPath, ';', dbgfR3PlugInLoadCallback, pPlugIn, pErrInfo);
              if (RT_FAILURE_NP(rc))
              {
                  /* 3. The VBOXDBG_PLUG_IN_PATH environment variable. */
                  rc2 = RTEnvGetEx(RTENV_DEFAULT, "VBOXDBG_PLUG_IN_PATH", szPath, sizeof(szPath), NULL);
                  if (RT_SUCCESS(rc2))
                      rc = RTPathTraverseList(szPath, ';', dbgfR3PlugInLoadCallback, pPlugIn, pErrInfo);
              }
          }
  
          if (rc == VERR_END_OF_STRING)
              rc = VERR_FILE_NOT_FOUND;
          if (pErrInfo && !RTErrInfoIsSet(pErrInfo))
              RTErrInfoSetF(pErrInfo, rc, "Failed to locate '%s'", pPlugIn->szName);
      }
      if (RT_SUCCESS(rc))
      {
          /*
           * Try initialize it.
           */
          rc = pPlugIn->pfnEntry(DBGFPLUGINOP_INIT, pUVM, VBOX_VERSION);
          if (RT_SUCCESS(rc))
          {
              /*
               * Link it and we're good.
               */
              pPlugIn->pNext = pUVM->dbgf.s.pPlugInHead;
              pUVM->dbgf.s.pPlugInHead = pPlugIn;
  
              DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
              return VINF_SUCCESS;
          }
  
          RTErrInfoSet(pErrInfo, rc, "Plug-in init failed");
          LogRel(("DBGF: Plug-in '%s' failed during init: %Rrc\n", pPlugIn->szName, rc));
          RTLdrClose(pPlugIn->hLdrMod);
      }
      MMR3HeapFree(pPlugIn);
  
      DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
      return rc;
  }
  
  
  /**
   * Load a debugging plug-in.
   *
   * @returns VBox status code.
   * @retval  VERR_ALREADY_EXISTS if the module was already loaded.
   * @retval  VINF_BUFFER_OVERFLOW if the actual plug-in name buffer was too small
   *          (the plug-in was still successfully loaded).
   * @param   pUVM        Pointer to the user-mode VM structure.
   * @param   pszPlugIn   The plug-in name.  This may specify the exact path to
   *                      the plug-in module, or it may just specify the core name
   *                      of the plug-in without prefix, suffix and path.
   * @param   pszActual   Buffer to return the actual plug-in name in. Optional.
   *                      This will be returned on VERR_ALREADY_EXSIST too.
   * @param   cbActual    The size of @a pszActual.
   * @param   pErrInfo    Optional error information structure.
   */
  VMMR3DECL(int) DBGFR3PlugInLoad(PUVM pUVM, const char *pszPlugIn, char *pszActual, size_t cbActual, PRTERRINFO pErrInfo)
  {
      UVM_ASSERT_VALID_EXT_RETURN(pUVM, VERR_INVALID_VM_HANDLE);
      AssertPtrReturn(pszPlugIn, VERR_INVALID_PARAMETER);
  
      /*
       * Extract the plug-in name.  Copy it to the return buffer as we'll want to
       * return it in the VERR_ALREADY_EXISTS case too.
       */
      char szName[DBGFPLUGIN_MAX_NAME];
      int rc = dbgfPlugInExtractName(szName, pszPlugIn, pErrInfo);
      if (RT_SUCCESS(rc))
      {
          int rc2 = VINF_SUCCESS;
          if (pszActual)
              rc2 = RTStrCopy(pszActual, cbActual, szName);
  
          /*
           * Write lock releated DBGF bits and try load it.
           */
          rc = VMR3ReqPriorityCallWaitU(pUVM, 0 /*idDstCpu*/, (PFNRT)dbgfR3PlugInLoad, 4, pUVM, szName, pszPlugIn, pErrInfo);
          if (rc2 != VINF_SUCCESS && RT_SUCCESS(rc))
              rc = VINF_BUFFER_OVERFLOW;
      }
  
      return rc;
  }
  
  
  /**
   * Load all plug-ins from the architechture private directory of VBox.
   *
   * @param   pUVM    Pointer to the user-mode VM structure.
   */
  VMMR3DECL(void) DBGFR3PlugInLoadAll(PUVM pUVM)
  {
      UVM_ASSERT_VALID_EXT_RETURN_VOID(pUVM);
  
      /*
       * Pass it on to EMT(0) if necessary (thanks to DBGFR3Os*).
       */
      if (VMR3GetVMCPUId(pUVM->pVM) != 0)
      {
          VMR3ReqPriorityCallVoidWaitU(pUVM, 0 /*idDstCpu*/, (PFNRT)DBGFR3PlugInLoadAll, 1, pUVM);
          return;
      }
  
  
      /*
       * Open the architecture specific directory with a filter on our prefix
       * and names including a dot.
       */
      const char *pszSuff = RTLdrGetSuff();
      size_t      cchSuff = strlen(pszSuff);
  
      char szPath[RTPATH_MAX];
      int rc = RTPathAppPrivateArch(szPath, sizeof(szPath) - cchSuff);
      AssertRCReturnVoid(rc);
      size_t offDir = strlen(szPath);
  
      rc = RTPathAppend(szPath, sizeof(szPath) - cchSuff, DBGF_PLUG_IN_PREFIX "*");
      AssertRCReturnVoid(rc);
      strcat(szPath, pszSuff);
  
      RTDIR hDir;
      rc = RTDirOpenFiltered(&hDir, szPath, RTDIRFILTER_WINNT, 0 /*fFlags*/);
      if (RT_SUCCESS(rc))
      {
          /*
           * Now read it and try load each of the plug-in modules.
           */
          RTDIRENTRY DirEntry;
          while (RT_SUCCESS(RTDirRead(hDir, &DirEntry, NULL)))
          {
              szPath[offDir] = '\0';
              rc = RTPathAppend(szPath, sizeof(szPath), DirEntry.szName);
              if (RT_SUCCESS(rc))
              {
                  char szName[DBGFPLUGIN_MAX_NAME];
                  rc = dbgfPlugInExtractName(szName, DirEntry.szName, NULL);
                  if (RT_SUCCESS(rc))
                  {
                      DBGF_PLUG_IN_WRITE_LOCK(pUVM);
                      dbgfR3PlugInLoad(pUVM, szName, szPath, NULL);
                      DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
                  }
              }
          }
  
          RTDirClose(hDir);
      }
  }
  
  
  /**
   * Unloads a plug-in by name (no path, prefix or suffix).
   *
   * @returns VBox status code.
   * @retval  VERR_NOT_FOUND if the specified plug-in wasn't found.
   * @param   pUVM        Pointer to the user-mode VM structure.
   * @param   pszName     The name of the plug-in to unload.
   */
  VMMR3DECL(int) DBGFR3PlugInUnload(PUVM pUVM, const char *pszName)
  {
      UVM_ASSERT_VALID_EXT_RETURN(pUVM, VERR_INVALID_VM_HANDLE);
  
      /*
       * Pass it on to EMT(0) if necessary (thanks to DBGFR3Os*).
       */
      if (VMR3GetVMCPUId(pUVM->pVM) != 0)
          return VMR3ReqPriorityCallWaitU(pUVM, 0 /*idDstCpu*/, (PFNRT)DBGFR3PlugInUnload, 2, pUVM, pszName);
  
  
      /*
       * Find the plug-in.
       */
      DBGF_PLUG_IN_WRITE_LOCK(pUVM);
  
      int rc;
      PDBGFPLUGIN pPrevPlugIn;
      PDBGFPLUGIN pPlugIn = dbgfR3PlugInLocate(pUVM, pszName, &pPrevPlugIn);
      if (pPlugIn)
      {
          /*
           * Unlink, terminate, unload and free the plug-in.
           */
          if (pPrevPlugIn)
              pPrevPlugIn->pNext = pPlugIn->pNext;
          else
              pUVM->dbgf.s.pPlugInHead = pPlugIn->pNext;
  
          pPlugIn->pfnEntry(DBGFPLUGINOP_TERM, pUVM, 0);
          RTLdrClose(pPlugIn->hLdrMod);
  
          pPlugIn->pfnEntry = NULL;
          pPlugIn->hLdrMod  = NIL_RTLDRMOD;
          MMR3HeapFree(pPlugIn->pNext);
          rc = VINF_SUCCESS;
      }
      else
          rc = VERR_NOT_FOUND;
  
      DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
      return rc;
  }
  
  
  /**
   * Unload all plug-ins.
   *
   * @param   pUVM    Pointer to the user-mode VM structure.
   */
  static DECLCALLBACK(void) dbgfPlugInUnloadAll(PUVM pUVM)
  {
      DBGF_PLUG_IN_WRITE_LOCK(pUVM);
  
      while (pUVM->dbgf.s.pPlugInHead)
      {
          PDBGFPLUGIN pPlugin = pUVM->dbgf.s.pPlugInHead;
          pUVM->dbgf.s.pPlugInHead = pPlugin->pNext;
  
          pPlugin->pfnEntry(DBGFPLUGINOP_TERM, pUVM, 0);
  
          int rc2 = RTLdrClose(pPlugin->hLdrMod);
          AssertRC(rc2);
  
          pPlugin->pfnEntry = NULL;
          pPlugin->hLdrMod  = NIL_RTLDRMOD;
          MMR3HeapFree(pPlugin);
      }
  
      DBGF_PLUG_IN_WRITE_UNLOCK(pUVM);
  }
  
  
  /**
   * Unloads all plug-ins.
   *
   * @param   pUVM    Pointer to the user-mode VM structure.
   */
  VMMR3DECL(void) DBGFR3PlugInUnloadAll(PUVM pUVM)
  {
      UVM_ASSERT_VALID_EXT_RETURN_VOID(pUVM);
      /* Thanks to DBGFR3Os, this must be done on EMT(0). */
      VMR3ReqPriorityCallVoidWaitU(pUVM, 0 /*idDstCpu*/, (PFNRT)dbgfPlugInUnloadAll, 1, pUVM);
  }
  
  
  
  /**
   * @callback_method_impl{FNDBGFHANDLERINT, The 'plugins' info item.}
   */
  static DECLCALLBACK(void) dbgfR3PlugInInfoList(PVM pVM, PCDBGFINFOHLP pHlp, const char *pszArgs)
  {
      PDBGFPLUGIN pPlugIn = pVM->pUVM->dbgf.s.pPlugInHead;
      RT_NOREF_PV(pszArgs);
      if (pPlugIn)
      {
          pHlp->pfnPrintf(pHlp, "Debugging plug-in%s: %s", pPlugIn->pNext ? "s" : "", pPlugIn->szName);
          while ((pPlugIn = pPlugIn->pNext) != NULL)
              pHlp->pfnPrintf(pHlp, ", %s", pPlugIn->szName);
          pHlp->pfnPrintf(pHlp, "\n");
  
      }
      else
          pHlp->pfnPrintf(pHlp, "No plug-ins loaded\n");
  }
  
  