using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; namespace ProfilerHelpers { public class MethodStore : IDisposable { // JITed methods information (start address + size + signature) private readonly List _methods; // addresses from callstacks already matching (address -> full name) private readonly Dictionary _cache; // for native methods, rely on dbghelp API // so the process handle is required private IntPtr _hProcess; private Process _process; private readonly int _pid; public MethodStore(int pid, bool loadModules = false) { // it may be possible to open the process // in that case, _hProcess = IntPtr.Zero _pid = pid; _methods = new List(1024); _cache = new Dictionary(); _hProcess = BindToProcess(pid, loadModules); } private IntPtr BindToProcess(int pid, bool loadModules) { try { _process = Process.GetProcessById(pid); if (!SymInitialize(_process.Handle, loadModules)) return IntPtr.Zero; return _process.Handle; } catch (Exception x) { Console.WriteLine($"Error while binding pid #{pid} to DbgHelp:"); Console.WriteLine(x.Message); return IntPtr.Zero; } } private bool SymInitialize(IntPtr hProcess, bool loadModules = false) { // read https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetoptions for more details // maybe SYMOPT_NO_PROMPTS and SYMOPT_FAIL_CRITICAL_ERRORS could be used NativeDbgHelp.SymSetOptions( NativeDbgHelp.SYMOPT_DEFERRED_LOADS | // performance optimization NativeDbgHelp.SYMOPT_UNDNAME // C++ names are not mangled ); // https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-syminitialize // search path for symbols: // - The current working directory of the application // - The _NT_SYMBOL_PATH environment variable // - The _NT_ALTERNATE_SYMBOL_PATH environment variable // // passing false as last parameter means that we will need to call SymLoadModule64 // each time a module is loaded in the process return NativeDbgHelp.SymInitialize(hProcess, null, loadModules); } public MethodInfo Add(ulong address, int size, string namespaceAndTypeName, string name, string signature) { var method = new MethodInfo(address, size, namespaceAndTypeName, name, signature); _methods.Add(method); return method; } public string GetFullName(ulong address) { if (_cache.TryGetValue(address, out var fullName)) return fullName; // look for managed methods for (int i = 0; i < _methods.Count; i++) { var method = _methods[i]; if ((address >= method.StartAddress) && (address < method.StartAddress + (ulong)method.Size)) { fullName = method.FullName; _cache[address] = fullName; return fullName; } } // look for native methods fullName = GetNativeMethodName(address); _cache[address] = fullName; return fullName; } private string GetNativeMethodName(ulong address) { var symbol = new NativeDbgHelp.SYMBOL_INFO(); symbol.MaxNameLen = 1024; symbol.SizeOfStruct = (uint)Marshal.SizeOf(symbol) - 1024; // char buffer is not counted // the ANSI version of SymFromAddr is called so each character is 1 byte long if (NativeDbgHelp.SymFromAddr(_hProcess, address, out var displacement, ref symbol)) { var buffer = new StringBuilder(symbol.Name.Length); // remove weird "$##" at the end of some symbols var pos = symbol.Name.LastIndexOf("$##"); if (pos == -1) buffer.Append(symbol.Name); else buffer.Append(symbol.Name, 0, pos); // add offset if any if (displacement != 0) buffer.Append($"+0x{displacement}"); return buffer.ToString(); } // default value is just the address in HEX #if DEBUG return ($"0x{address:x} (SymFromAddr failed with 0x{Marshal.GetLastWin32Error():x})"); #else return $"0x{address:x}"; #endif } const int ERROR_SUCCESS = 0; public void AddModule(string filename, ulong baseOfDll, int sizeOfDll) { var baseAddress = NativeDbgHelp.SymLoadModule64(_hProcess, IntPtr.Zero, filename, null, baseOfDll, (uint)sizeOfDll); if (baseAddress == 0) { // should work if the same module is added more than once if (Marshal.GetLastWin32Error() == ERROR_SUCCESS) return; Console.WriteLine($"SymLoadModule64 failed for {filename}"); } } public void Dispose() { if (_hProcess == IntPtr.Zero) return; _hProcess = IntPtr.Zero; _process.Dispose(); } } internal static class NativeDbgHelp { // from C:\Program Files (x86)\Windows Kits\10\Debuggers\inc\dbghelp.h public const uint SYMOPT_UNDNAME = 0x00000002; public const uint SYMOPT_DEFERRED_LOADS = 0x00000004; [StructLayout(LayoutKind.Sequential)] public struct SYMBOL_INFO { public uint SizeOfStruct; public uint TypeIndex; // Type Index of symbol private ulong Reserved1; private ulong Reserved2; public uint Index; public uint Size; public ulong ModBase; // Base Address of module containing this symbol public uint Flags; public ulong Value; // Value of symbol, ValuePresent should be 1 public ulong Address; // Address of symbol including base address of module public uint Register; // register holding value or pointer to value public uint Scope; // scope of the symbol public uint Tag; // pdb classification public uint NameLen; // Actual length of name public uint MaxNameLen; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] public string Name; } [DllImport("dbghelp.dll", SetLastError = true)] public static extern bool SymInitialize(IntPtr hProcess, string userSearchPath, bool invadeProcess); [DllImport("dbghelp.dll", SetLastError = true)] public static extern uint SymSetOptions(uint symOptions); [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Ansi)] public static extern ulong SymLoadModule64(IntPtr hProcess, IntPtr hFile, string imageName, string moduleName, ulong baseOfDll, uint sizeOfDll); // use ANSI version to ensure the right size of the structure // read https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_info [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Ansi)] public static extern bool SymFromAddr(IntPtr hProcess, ulong address, out ulong displacement, ref SYMBOL_INFO symbol); [DllImport("dbghelp.dll", SetLastError = true)] public static extern bool SymCleanup(IntPtr hProcess); } }