summaryrefslogtreecommitdiffstats
path: root/src/VBox/Runtime/tools/RTLdrFlt.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/Runtime/tools/RTLdrFlt.cpp')
-rw-r--r--src/VBox/Runtime/tools/RTLdrFlt.cpp598
1 files changed, 598 insertions, 0 deletions
diff --git a/src/VBox/Runtime/tools/RTLdrFlt.cpp b/src/VBox/Runtime/tools/RTLdrFlt.cpp
new file mode 100644
index 00000000..57fd4ac7
--- /dev/null
+++ b/src/VBox/Runtime/tools/RTLdrFlt.cpp
@@ -0,0 +1,598 @@
+/* $Id: RTLdrFlt.cpp $ */
+/** @file
+ * IPRT - Utility for translating addresses into symbols+offset.
+ */
+
+/*
+ * Copyright (C) 2006-2022 Oracle and/or its affiliates.
+ *
+ * This file is part of VirtualBox base platform packages, as
+ * available from https://www.virtualbox.org.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation, in version 3 of the
+ * License.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses>.
+ *
+ * The contents of this file may alternatively be used under the terms
+ * of the Common Development and Distribution License Version 1.0
+ * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
+ * in the VirtualBox distribution, in which case the provisions of the
+ * CDDL are applicable instead of those of the GPL.
+ *
+ * You may elect to license modified versions of this file under the
+ * terms and conditions of either the GPL or the CDDL or both.
+ *
+ * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
+ */
+
+
+/*********************************************************************************************************************************
+* Header Files *
+*********************************************************************************************************************************/
+#include <iprt/mem.h>
+#include <iprt/assert.h>
+#include <iprt/ctype.h>
+#include <iprt/dbg.h>
+#include <iprt/err.h>
+#include <iprt/getopt.h>
+#include <iprt/initterm.h>
+#include <iprt/message.h>
+#include <iprt/path.h>
+#include <iprt/stream.h>
+#include <iprt/string.h>
+
+
+
+/** Worker for ProduceKAllSyms. */
+static void PrintSymbolForKAllSyms(const char *pszModule, PCRTDBGSYMBOL pSymInfo, PCRTDBGSEGMENT pSegInfo,
+ RTUINTPTR uBaseAddr, bool fOneSeg)
+{
+ RTUINTPTR uAddr;
+ char chType = 't';
+ if (pSymInfo->iSeg < RTDBGSEGIDX_SPECIAL_FIRST)
+ {
+ uAddr = uBaseAddr + pSymInfo->offSeg;
+ if (!fOneSeg)
+ uAddr += pSegInfo->uRva;
+ if (pSegInfo->szName[0])
+ {
+ if (strstr(pSegInfo->szName, "rodata") != NULL)
+ chType = 'r';
+ else if (strstr(pSegInfo->szName, "bss") != NULL)
+ chType = 'b';
+ else if (strstr(pSegInfo->szName, "data") != NULL)
+ chType = 'd';
+ }
+ }
+ else if (pSymInfo->iSeg == RTDBGSEGIDX_ABS)
+ {
+ chType = 'a';
+ uAddr = pSymInfo->offSeg;
+ }
+ else if (pSymInfo->iSeg == RTDBGSEGIDX_RVA)
+ {
+ Assert(!fOneSeg);
+ uAddr = uBaseAddr + pSymInfo->offSeg;
+ }
+ else
+ {
+ RTMsgError("Unsupported special segment %#x for %s in %s!", pSymInfo->iSeg, pSymInfo->szName, pszModule);
+ return;
+ }
+
+ RTPrintf("%RTptr %c %s\t[%s]\n", uAddr, chType, pSymInfo->szName, pszModule);
+}
+
+
+/**
+ * Produces a /proc/kallsyms compatible symbol listing of @a hDbgAs on standard
+ * output.
+ *
+ * @returns Exit code.
+ * @param hDbgAs The address space to dump.
+ */
+static RTEXITCODE ProduceKAllSyms(RTDBGAS hDbgAs)
+{
+ /*
+ * Iterate modules.
+ */
+ uint32_t cModules = RTDbgAsModuleCount(hDbgAs);
+ for (uint32_t iModule = 0; iModule < cModules; iModule++)
+ {
+ RTDBGMOD const hDbgMod = RTDbgAsModuleByIndex(hDbgAs, iModule);
+ const char * const pszModule = RTDbgModName(hDbgMod);
+
+ /*
+ * Iterate mappings of the module.
+ */
+ RTDBGASMAPINFO aMappings[128];
+ uint32_t cMappings = RT_ELEMENTS(aMappings);
+ int rc = RTDbgAsModuleQueryMapByIndex(hDbgAs, iModule, &aMappings[0], &cMappings, 0 /*fFlags*/);
+ if (RT_SUCCESS(rc))
+ {
+ for (uint32_t iMapping = 0; iMapping < cMappings; iMapping++)
+ {
+ RTDBGSEGMENT SegInfo = {0};
+ if (aMappings[iMapping].iSeg == NIL_RTDBGSEGIDX)
+ {
+ /*
+ * Flat mapping of the entire module.
+ */
+ SegInfo.iSeg = NIL_RTDBGSEGIDX;
+ uint32_t cSymbols = RTDbgModSymbolCount(hDbgMod);
+ for (uint32_t iSymbol = 0; iSymbol < cSymbols; iSymbol++)
+ {
+ RTDBGSYMBOL SymInfo;
+ rc = RTDbgModSymbolByOrdinal(hDbgMod, iSymbol, &SymInfo);
+ if (RT_SUCCESS(rc))
+ {
+ if ( SymInfo.iSeg != SegInfo.iSeg
+ && SymInfo.iSeg < RTDBGSEGIDX_SPECIAL_FIRST)
+ {
+ rc = RTDbgModSegmentByIndex(hDbgMod, SymInfo.iSeg, &SegInfo);
+ if (RT_FAILURE(rc))
+ {
+ RTMsgError("RTDbgModSegmentByIndex(%s, %u) failed: %Rrc", pszModule, SymInfo.iSeg, rc);
+ continue;
+ }
+ }
+ PrintSymbolForKAllSyms(pszModule, &SymInfo, &SegInfo, aMappings[iMapping].Address, false);
+ }
+ else
+ RTMsgError("RTDbgModSymbolByOrdinal(%s, %u) failed: %Rrc", pszModule, iSymbol, rc);
+ }
+ }
+ else
+ {
+ /*
+ * Just one segment.
+ */
+ rc = RTDbgModSegmentByIndex(hDbgMod, aMappings[iMapping].iSeg, &SegInfo);
+ if (RT_SUCCESS(rc))
+ {
+ /** @todo */
+ }
+ else
+ RTMsgError("RTDbgModSegmentByIndex(%s, %u) failed: %Rrc", pszModule, aMappings[iMapping].iSeg, rc);
+ }
+ }
+ }
+ else
+ RTMsgError("RTDbgAsModuleQueryMapByIndex failed: %Rrc", rc);
+ RTDbgModRelease(hDbgMod);
+ }
+
+ return RTEXITCODE_SUCCESS;
+}
+
+
+/**
+ * Dumps the address space.
+ */
+static void DumpAddressSpace(RTDBGAS hDbgAs, unsigned cVerbosityLevel)
+{
+ RTPrintf("*** Address Space Dump ***\n");
+ uint32_t cModules = RTDbgAsModuleCount(hDbgAs);
+ for (uint32_t iModule = 0; iModule < cModules; iModule++)
+ {
+ RTDBGMOD hDbgMod = RTDbgAsModuleByIndex(hDbgAs, iModule);
+ RTPrintf("Module #%u: %s\n", iModule, RTDbgModName(hDbgMod));
+
+ RTDBGASMAPINFO aMappings[128];
+ uint32_t cMappings = RT_ELEMENTS(aMappings);
+ int rc = RTDbgAsModuleQueryMapByIndex(hDbgAs, iModule, &aMappings[0], &cMappings, 0 /*fFlags*/);
+ if (RT_SUCCESS(rc))
+ {
+ for (uint32_t iMapping = 0; iMapping < cMappings; iMapping++)
+ {
+ if (aMappings[iMapping].iSeg == NIL_RTDBGSEGIDX)
+ {
+ RTPrintf(" mapping #%u: %RTptr-%RTptr\n",
+ iMapping,
+ aMappings[iMapping].Address,
+ aMappings[iMapping].Address + RTDbgModImageSize(hDbgMod) - 1);
+ if (cVerbosityLevel > 2)
+ {
+ uint32_t cSegments = RTDbgModSegmentCount(hDbgMod);
+ for (uint32_t iSeg = 0; iSeg < cSegments; iSeg++)
+ {
+ RTDBGSEGMENT SegInfo;
+ rc = RTDbgModSegmentByIndex(hDbgMod, iSeg, &SegInfo);
+ if (RT_SUCCESS(rc))
+ RTPrintf(" seg #%u: %RTptr LB %RTptr '%s'\n",
+ iSeg, SegInfo.uRva, SegInfo.cb, SegInfo.szName);
+ else
+ RTPrintf(" seg #%u: %Rrc\n", iSeg, rc);
+ }
+ }
+ }
+ else
+ {
+ RTDBGSEGMENT SegInfo;
+ rc = RTDbgModSegmentByIndex(hDbgMod, aMappings[iMapping].iSeg, &SegInfo);
+ if (RT_SUCCESS(rc))
+ RTPrintf(" mapping #%u: %RTptr-%RTptr (segment #%u - '%s')\n",
+ iMapping,
+ aMappings[iMapping].Address,
+ aMappings[iMapping].Address + SegInfo.cb,
+ SegInfo.iSeg, SegInfo.szName);
+ else
+ RTPrintf(" mapping #%u: %RTptr-???????? (segment #%u) rc=%Rrc\n",
+ iMapping, aMappings[iMapping].Address, aMappings[iMapping].iSeg, rc);
+ }
+
+ if (cVerbosityLevel > 1)
+ {
+ uint32_t cSymbols = RTDbgModSymbolCount(hDbgMod);
+ RTPrintf(" %u symbols\n", cSymbols);
+ for (uint32_t iSymbol = 0; iSymbol < cSymbols; iSymbol++)
+ {
+ RTDBGSYMBOL SymInfo;
+ rc = RTDbgModSymbolByOrdinal(hDbgMod, iSymbol, &SymInfo);
+ if (RT_SUCCESS(rc))
+ RTPrintf(" #%04u at %08x:%RTptr (%RTptr) %05llx %s\n",
+ SymInfo.iOrdinal, SymInfo.iSeg, SymInfo.offSeg, SymInfo.Value,
+ (uint64_t)SymInfo.cb, SymInfo.szName);
+ }
+ }
+ }
+ }
+ else
+ RTMsgError("RTDbgAsModuleQueryMapByIndex failed: %Rrc", rc);
+ RTDbgModRelease(hDbgMod);
+ }
+ RTPrintf("*** End of Address Space Dump ***\n");
+}
+
+
+/**
+ * Tries to parse out an address at the head of the string.
+ *
+ * @returns true if found address, false if not.
+ * @param psz Where to start parsing.
+ * @param pcchAddress Where to store the address length.
+ * @param pu64Address Where to store the address value.
+ */
+static bool TryParseAddress(const char *psz, size_t *pcchAddress, uint64_t *pu64Address)
+{
+ const char *pszStart = psz;
+
+ /*
+ * Hex prefix?
+ */
+ if (psz[0] == '0' && (psz[1] == 'x' || psz[1] == 'X'))
+ psz += 2;
+
+ /*
+ * How many hex digits? We want at least 4 and at most 16.
+ */
+ size_t off = 0;
+ while (RT_C_IS_XDIGIT(psz[off]))
+ off++;
+ if (off < 4 || off > 16)
+ return false;
+
+ /*
+ * Check for separator (xxxxxxxx'yyyyyyyy).
+ */
+ bool fHave64bitSep = off <= 8
+ && psz[off] == '\''
+ && RT_C_IS_XDIGIT(psz[off + 1])
+ && RT_C_IS_XDIGIT(psz[off + 2])
+ && RT_C_IS_XDIGIT(psz[off + 3])
+ && RT_C_IS_XDIGIT(psz[off + 4])
+ && RT_C_IS_XDIGIT(psz[off + 5])
+ && RT_C_IS_XDIGIT(psz[off + 6])
+ && RT_C_IS_XDIGIT(psz[off + 7])
+ && RT_C_IS_XDIGIT(psz[off + 8])
+ && !RT_C_IS_XDIGIT(psz[off + 9]);
+ if (fHave64bitSep)
+ {
+ uint32_t u32High;
+ int rc = RTStrToUInt32Ex(psz, NULL, 16, &u32High);
+ if (rc != VWRN_TRAILING_CHARS)
+ return false;
+
+ uint32_t u32Low;
+ rc = RTStrToUInt32Ex(&psz[off + 1], NULL, 16, &u32Low);
+ if ( rc != VINF_SUCCESS
+ && rc != VWRN_TRAILING_SPACES
+ && rc != VWRN_TRAILING_CHARS)
+ return false;
+
+ *pu64Address = RT_MAKE_U64(u32Low, u32High);
+ off += 1 + 8;
+ }
+ else
+ {
+ int rc = RTStrToUInt64Ex(psz, NULL, 16, pu64Address);
+ if ( rc != VINF_SUCCESS
+ && rc != VWRN_TRAILING_SPACES
+ && rc != VWRN_TRAILING_CHARS)
+ return false;
+ }
+
+ *pcchAddress = psz + off - pszStart;
+ return true;
+}
+
+
+int main(int argc, char **argv)
+{
+ int rc = RTR3InitExe(argc, &argv, 0);
+ if (RT_FAILURE(rc))
+ return RTMsgInitFailure(rc);
+
+ /*
+ * Create an empty address space that we can load modules and stuff into
+ * as we parse the parameters.
+ */
+ RTDBGAS hDbgAs;
+ rc = RTDbgAsCreate(&hDbgAs, 0, RTUINTPTR_MAX, "");
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDBgAsCreate -> %Rrc", rc);
+
+ /*
+ * Create a debugging configuration instance to work with so that we can
+ * make use of (i.e. test) path searching and such.
+ */
+ RTDBGCFG hDbgCfg;
+ rc = RTDbgCfgCreate(&hDbgCfg, "IPRT", true /*fNativePaths*/);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDbgCfgCreate -> %Rrc", rc);
+
+ /*
+ * Parse arguments.
+ */
+ static const RTGETOPTDEF s_aOptions[] =
+ {
+ { "--input", 'i', RTGETOPT_REQ_STRING },
+ { "--local-file", 'l', RTGETOPT_REQ_NOTHING },
+ { "--cache-file", 'c', RTGETOPT_REQ_NOTHING },
+ { "--pe-image", 'p', RTGETOPT_REQ_NOTHING },
+ { "--verbose", 'v', RTGETOPT_REQ_NOTHING },
+ { "--x86", '8', RTGETOPT_REQ_NOTHING },
+ { "--amd64", '6', RTGETOPT_REQ_NOTHING },
+ { "--whatever", '*', RTGETOPT_REQ_NOTHING },
+ { "--kallsyms", 'k', RTGETOPT_REQ_NOTHING },
+ };
+
+ PRTSTREAM pInput = g_pStdIn;
+ PRTSTREAM pOutput = g_pStdOut;
+ unsigned cVerbosityLevel = 0;
+ enum {
+ kOpenMethod_FromImage,
+ kOpenMethod_FromPeImage
+ } enmOpenMethod = kOpenMethod_FromImage;
+ bool fCacheFile = false;
+ RTLDRARCH enmArch = RTLDRARCH_WHATEVER;
+ bool fKAllSyms = false;
+
+ RTGETOPTUNION ValueUnion;
+ RTGETOPTSTATE GetState;
+ RTGetOptInit(&GetState, argc, argv, s_aOptions, RT_ELEMENTS(s_aOptions), 1, 0);
+ while ((rc = RTGetOpt(&GetState, &ValueUnion)))
+ {
+ switch (rc)
+ {
+ case 'i':
+ rc = RTStrmOpen(ValueUnion.psz, "r", &pInput);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "Failed to open '%s' for reading: %Rrc", ValueUnion.psz, rc);
+ break;
+
+ case 'c':
+ fCacheFile = true;
+ break;
+
+ case 'k':
+ fKAllSyms = true;
+ break;
+
+ case 'l':
+ fCacheFile = false;
+ break;
+
+ case 'p':
+ enmOpenMethod = kOpenMethod_FromPeImage;
+ break;
+
+ case 'v':
+ cVerbosityLevel++;
+ break;
+
+ case '8':
+ enmArch = RTLDRARCH_X86_32;
+ break;
+
+ case '6':
+ enmArch = RTLDRARCH_AMD64;
+ break;
+
+ case '*':
+ enmArch = RTLDRARCH_WHATEVER;
+ break;
+
+ case 'h':
+ RTPrintf("Usage: %s [options] <module> <address> [<module> <address> [..]]\n"
+ "\n"
+ "Options:\n"
+ " -i,--input=file\n"
+ " Specify a input file instead of standard input.\n"
+ " --pe-image\n"
+ " Use RTDbgModCreateFromPeImage to open the file."
+ " -v, --verbose\n"
+ " Display the address space before doing the filtering.\n"
+ " --amd64,--x86,--whatever\n"
+ " Selects the desired architecture.\n"
+ " -k,--kallsyms\n"
+ " Produce a /proc/kallsyms compatible symbol listing and quit.\n"
+ " -h, -?, --help\n"
+ " Display this help text and exit successfully.\n"
+ " -V, --version\n"
+ " Display the revision and exit successfully.\n"
+ , RTPathFilename(argv[0]));
+ return RTEXITCODE_SUCCESS;
+
+ case 'V':
+ RTPrintf("$Revision: 153224 $\n");
+ return RTEXITCODE_SUCCESS;
+
+ case VINF_GETOPT_NOT_OPTION:
+ {
+ /* <module> <address> */
+ const char *pszModule = ValueUnion.psz;
+
+ rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_UINT64 | RTGETOPT_FLAG_HEX);
+ if (RT_FAILURE(rc))
+ return RTGetOptPrintError(rc, &ValueUnion);
+ uint64_t u64Address = ValueUnion.u64;
+
+ uint32_t cbImage = 0;
+ uint32_t uTimestamp = 0;
+ if (fCacheFile)
+ {
+ rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_UINT32 | RTGETOPT_FLAG_HEX);
+ if (RT_FAILURE(rc))
+ return RTGetOptPrintError(rc, &ValueUnion);
+ cbImage = ValueUnion.u32;
+
+ rc = RTGetOptFetchValue(&GetState, &ValueUnion, RTGETOPT_REQ_UINT32 | RTGETOPT_FLAG_HEX);
+ if (RT_FAILURE(rc))
+ return RTGetOptPrintError(rc, &ValueUnion);
+ uTimestamp = ValueUnion.u32;
+ }
+
+ RTDBGMOD hMod;
+ if (enmOpenMethod == kOpenMethod_FromImage)
+ rc = RTDbgModCreateFromImage(&hMod, pszModule, NULL, enmArch, hDbgCfg);
+ else
+ rc = RTDbgModCreateFromPeImage(&hMod, pszModule, NULL, NULL, cbImage, uTimestamp, hDbgCfg);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDbgModCreateFromImage(,%s,,) -> %Rrc", pszModule, rc);
+
+ rc = RTDbgAsModuleLink(hDbgAs, hMod, u64Address, 0 /* fFlags */);
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTDbgAsModuleLink(,%s,%llx,) -> %Rrc", pszModule, u64Address, rc);
+ break;
+ }
+
+ default:
+ return RTGetOptPrintError(rc, &ValueUnion);
+ }
+ }
+
+ /*
+ * Display the address space.
+ */
+ if (cVerbosityLevel)
+ DumpAddressSpace(hDbgAs, cVerbosityLevel);
+
+ /*
+ * Produce the /proc/kallsyms output.
+ */
+ if (fKAllSyms)
+ return ProduceKAllSyms(hDbgAs);
+
+ /*
+ * Read text from standard input and see if there is anything we can translate.
+ */
+ for (;;)
+ {
+ /* Get a line. */
+ char szLine[_64K];
+ rc = RTStrmGetLine(pInput, szLine, sizeof(szLine));
+ if (rc == VERR_EOF)
+ break;
+ if (RT_FAILURE(rc))
+ return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTStrmGetLine() -> %Rrc\n", rc);
+
+ /*
+ * Search the line for potential addresses and replace them with
+ * symbols+offset.
+ */
+ const char *pszStart = szLine;
+ const char *psz = szLine;
+ char ch;
+ while ((ch = *psz) != '\0')
+ {
+ size_t cchAddress;
+ uint64_t u64Address;
+
+ if ( ( ch == '0'
+ && (psz[1] == 'x' || psz[1] == 'X')
+ && TryParseAddress(psz, &cchAddress, &u64Address))
+ || ( RT_C_IS_XDIGIT(ch)
+ && TryParseAddress(psz, &cchAddress, &u64Address))
+ )
+ {
+ /* Print. */
+ psz += cchAddress;
+ if (pszStart != psz)
+ RTStrmWrite(pOutput, pszStart, psz - pszStart);
+ pszStart = psz;
+
+ /* Try get the module. */
+ RTUINTPTR uAddr;
+ RTDBGSEGIDX iSeg;
+ RTDBGMOD hDbgMod;
+ rc = RTDbgAsModuleByAddr(hDbgAs, u64Address, &hDbgMod, &uAddr, &iSeg);
+ if (RT_SUCCESS(rc))
+ {
+ if (iSeg != UINT32_MAX)
+ RTStrmPrintf(pOutput, "=[%s:%u", RTDbgModName(hDbgMod), iSeg);
+ else
+ RTStrmPrintf(pOutput, "=[%s", RTDbgModName(hDbgMod));
+
+ /*
+ * Do we have symbols?
+ */
+ RTDBGSYMBOL Symbol;
+ RTINTPTR offSym;
+ rc = RTDbgAsSymbolByAddr(hDbgAs, u64Address, RTDBGSYMADDR_FLAGS_LESS_OR_EQUAL, &offSym, &Symbol, NULL);
+ if (RT_SUCCESS(rc))
+ {
+ if (!offSym)
+ RTStrmPrintf(pOutput, "!%s", Symbol.szName);
+ else if (offSym > 0)
+ RTStrmPrintf(pOutput, "!%s+%#llx", Symbol.szName, offSym);
+ else
+ RTStrmPrintf(pOutput, "!%s-%#llx", Symbol.szName, -offSym);
+ }
+ else
+ RTStrmPrintf(pOutput, "+%#llx", u64Address - uAddr);
+
+ /*
+ * Do we have line numbers?
+ */
+ RTDBGLINE Line;
+ RTINTPTR offLine;
+ rc = RTDbgAsLineByAddr(hDbgAs, u64Address, &offLine, &Line, NULL);
+ if (RT_SUCCESS(rc))
+ RTStrmPrintf(pOutput, " %Rbn(%u)", Line.szFilename, Line.uLineNo);
+
+ RTStrmPrintf(pOutput, "]");
+ RTDbgModRelease(hDbgMod);
+ }
+ }
+ else
+ psz++;
+ }
+
+ if (pszStart != psz)
+ RTStrmWrite(pOutput, pszStart, psz - pszStart);
+ RTStrmPutCh(pOutput, '\n');
+ }
+
+ return RTEXITCODE_SUCCESS;
+}
+