src/Microsoft.Diagnostics.Runtime.Utilities/DbgEng/DbgEngIDataReader.cs (353 lines of code) (raw):

// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Microsoft.Diagnostics.Runtime.DataReaders.Implementation; namespace Microsoft.Diagnostics.Runtime.Utilities.DbgEng { public sealed class DbgEngIDataReader : IDataReader, IDisposable, IThreadReader { private const string VersionString = "@(#)Version "; private readonly IDisposable? _dbgeng; public IDebugClient DebugClient { get; } public IDebugControl DebugControl { get; } public IDebugDataSpaces DebugDataSpaces { get; } public IDebugAdvanced DebugAdvanced { get; } public IDebugSymbols DebugSymbols { get; } public IDebugSystemObjects DebugSystemObjects { get; } private bool _disposed; private List<ModuleInfo>? _modules; private int? _pointerSize; private Architecture? _architecture; public string DisplayName { get; } public OSPlatform TargetPlatform => DebugControl.OSPlatform; public static DataTarget CreateDataTarget(nint pDebugClient) { return new DataTarget(new CustomDataTarget(new DbgEngIDataReader(pDebugClient))); } public static DataTarget CreateDataTarget(object dbgeng) { return new DataTarget(new CustomDataTarget(new DbgEngIDataReader(dbgeng))); } /// <summary> /// Creates an instance of DbgEngIDataReader from the given object. This object /// must be castable to these interfaces: IDebugClient, IDebugControl, IDebugDataSpaces, /// IDebugAdvanced, IDebugSymbols, IDebugSystemObjects. /// /// The most common way to obtain a working version of this object is via IDebugClient.Create. /// </summary> /// <param name="dbgeng">An implementation of DbgEng interfaces.</param> public DbgEngIDataReader(object dbgeng) { DisplayName = $"DbgEng, DbgEng={dbgeng}"; _dbgeng = dbgeng as IDisposable; DebugClient = (IDebugClient)dbgeng; DebugControl = (IDebugControl)dbgeng; DebugDataSpaces = (IDebugDataSpaces)dbgeng; DebugAdvanced = (IDebugAdvanced)dbgeng; DebugSymbols = (IDebugSymbols)dbgeng; DebugSystemObjects = (IDebugSystemObjects)dbgeng; } public DbgEngIDataReader(nint pDebugClient) { if (pDebugClient == 0) throw new ArgumentNullException(nameof(pDebugClient)); DisplayName = $"DbgEng, IDebugClient={pDebugClient:x}"; _dbgeng = IDebugClient.Create(pDebugClient); DebugClient = (IDebugClient)_dbgeng; DebugControl = (IDebugControl)_dbgeng; DebugDataSpaces = (IDebugDataSpaces)_dbgeng; DebugAdvanced = (IDebugAdvanced)_dbgeng; DebugSymbols = (IDebugSymbols)_dbgeng; DebugSystemObjects = (IDebugSystemObjects)_dbgeng; } public DbgEngIDataReader(string dumpFile) { if (!File.Exists(dumpFile)) throw new FileNotFoundException(dumpFile); DisplayName = dumpFile; _dbgeng = IDebugClient.Create(); DebugSystemObjects = (IDebugSystemObjects)_dbgeng; DebugClient = (IDebugClient)_dbgeng; DebugControl = (IDebugControl)_dbgeng; DebugDataSpaces = (IDebugDataSpaces)_dbgeng; DebugAdvanced = (IDebugAdvanced)_dbgeng; DebugSymbols = (IDebugSymbols)_dbgeng; HResult hr = DebugClient.OpenDumpFile(dumpFile); if (hr != 0) { const int STATUS_MAPPED_FILE_SIZE_ZERO = unchecked((int)0xC000011E); if (hr == HResult.E_INVALIDARG || hr == (STATUS_MAPPED_FILE_SIZE_ZERO | 0x10000000)) throw new InvalidDataException($"'{dumpFile}' is not a crash dump."); throw CreateExceptionFromDumpFile(dumpFile, hr); } // This actually "attaches" to the crash dump. HResult result = DebugControl.WaitForEvent(TimeSpan.MaxValue); if (!result) throw CreateExceptionFromDumpFile(dumpFile, hr); } private static ClrDiagnosticsException CreateExceptionFromDumpFile(string dumpFile, HResult hr) { ClrDiagnosticsException ex = new($"Could not load crash dump, HRESULT: {hr}", hr); ex.Data["DumpFile"] = dumpFile; return ex; } public DbgEngIDataReader(int processId, DEBUG_ATTACH attach, TimeSpan timeout) { DisplayName = $"{processId:x}"; _dbgeng = IDebugClient.Create(); DebugSystemObjects = (IDebugSystemObjects)_dbgeng; DebugClient = (IDebugClient)_dbgeng; DebugControl = (IDebugControl)_dbgeng; DebugDataSpaces = (IDebugDataSpaces)_dbgeng; DebugAdvanced = (IDebugAdvanced)_dbgeng; DebugSymbols = (IDebugSymbols)_dbgeng; HResult hr = DebugControl.AddEngineOptions(DEBUG_ENGOPT.INITIAL_BREAK); if (hr) hr = DebugClient.AttachProcess(processId, attach); if (hr) hr = DebugControl.WaitForEvent(timeout); if (hr == HResult.S_FALSE) { throw new TimeoutException("Break in did not occur within the allotted timeout."); } if (hr != 0) { if ((uint)hr.Value == 0xd00000bb) throw new InvalidOperationException("Mismatched architecture between this process and the target process."); throw new ArgumentException($"Could not attach to process {processId}, HRESULT: 0x{hr:x}"); } } ~DbgEngIDataReader() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (_disposed) return; _disposed = true; if (disposing) { DebugClient.EndSession(DEBUG_END.ACTIVE_DETACH); DebugClient.DetachProcesses(); _dbgeng?.Dispose(); } } public bool IsThreadSafe => false; public int ProcessId => DebugSystemObjects.ProcessSystemId; public Architecture Architecture => _architecture ??= DebugControl.CpuType switch { ImageFileMachine.I386 => Architecture.X86, ImageFileMachine.AMD64 => Architecture.X64, ImageFileMachine.ARM or (ImageFileMachine)0x01c2 or // THUMB (ImageFileMachine)0x01c4 => Architecture.Arm, // THUMB2 (ImageFileMachine)0xAA64 => Architecture.Arm64, // ARM64 (ImageFileMachine)0x6264 => (Architecture)6 /* Architecture.LoongArch64 */, // LOONGARCH64 (ImageFileMachine)0x5064 => (Architecture)9 /* Architecture.RiscV64 */, // RISCV64 _ => (Architecture)(-1) }; public int PointerSize => _pointerSize ??= DebugControl.PointerSize; public void FlushCachedData() { _modules = null; } public bool GetThreadContext(uint systemId, uint contextFlags, Span<byte> context) { int curr = DebugSystemObjects.CurrentThreadId; try { if (DebugSystemObjects.GetThreadIdBySystemId((int)systemId, out int id) < 0) return false; DebugSystemObjects.CurrentThreadId = id; return DebugAdvanced.GetThreadContext(context) >= 0; } finally { DebugSystemObjects.CurrentThreadId = curr; } } private ulong[] GetImageBases() { HResult hr = DebugSymbols.GetNumberModules(out int count, out _); if (!hr) return Array.Empty<ulong>(); int index = 0; ulong[] bases = new ulong[count]; for (int i = 0; i < count; ++i) { hr = DebugSymbols.GetImageBase(i, out ulong baseAddress); if (hr) bases[index++] = baseAddress; } if (index < bases.Length) Array.Resize(ref bases, index); return bases; } public IEnumerable<ModuleInfo> EnumerateModules() { if (_modules != null) return _modules; ulong[] bases = GetImageBases(); if (bases.Length == 0) return Enumerable.Empty<ModuleInfo>(); DEBUG_MODULE_PARAMETERS[] moduleParams = new DEBUG_MODULE_PARAMETERS[bases.Length]; List<ModuleInfo> modules = new(); HResult hr = DebugSymbols.GetModuleParameters(bases, moduleParams); if (hr) { for (int i = 0; i < bases.Length; ++i) { DebugSymbols.GetModuleName(DEBUG_MODNAME.IMAGE, bases[i], out string moduleName); unchecked { // On Linux, getting the typical version information requires scanning through a large chunk of the memory for // a library looking for a particular string. This can be incredibly slow when there is a large number of modules. // Since ClrMD's ModuleInfo doesn't lazily calculate its Version, this means that the startup for Linux could be // incredibly slow. We should probably add a way to (optionally) lazily load the Version for modules to make this // more pay for play, but until then we will simply only return the version of libcoreclr.so on Linux, as that's // the bare minimum for functionality. Version? version = null; if (TargetPlatform == OSPlatform.Windows || moduleName.Contains("libcoreclr", StringComparison.OrdinalIgnoreCase)) GetVersionInfo(bases[i], moduleParams[i].Size); ModuleInfo? module = ModuleInfo.TryCreate(this, bases[i], moduleName, (int)moduleParams[i].Size, (int)moduleParams[i].TimeDateStamp, version); if (module is not null) modules.Add(module); } } } _modules = modules; return modules; } public int Read(ulong address, Span<byte> buffer) { if (!DebugDataSpaces.ReadVirtual(address, buffer, out int read)) return 0; return read; } public Version? GetVersionInfo(ulong baseAddress, uint moduleSize) { if (TargetPlatform == OSPlatform.Windows) { if (!FindModuleIndex(baseAddress, out int index)) return null; byte[] buffer = ArrayPool<byte>.Shared.Rent(256); try { HResult hr = DebugSymbols.GetModuleVersionInformation(index, baseAddress, "\\\\\0", buffer); if (!hr) return new Version(); int minor = Unsafe.As<byte, ushort>(ref buffer[8]); int major = Unsafe.As<byte, ushort>(ref buffer[10]); int patch = Unsafe.As<byte, ushort>(ref buffer[12]); int revision = Unsafe.As<byte, ushort>(ref buffer[14]); return new Version(major, minor, revision, patch); } finally { ArrayPool<byte>.Shared.Return(buffer); } } else { // GetModuleVersionInformation has a bug which we are working around here byte[] versionString = Encoding.ASCII.GetBytes(VersionString); if (DebugDataSpaces.Search(baseAddress, moduleSize, versionString, 1, out ulong offsetFound)) { byte[] bufferArray = ArrayPool<byte>.Shared.Rent(256); Span<byte> buffer = bufferArray; try { if (DebugDataSpaces.ReadVirtual(offsetFound + (uint)VersionString.Length, buffer, out int read)) { string versionStr = Encoding.ASCII.GetString(buffer[0..read]); int space = versionStr.IndexOf(' '); if (space > 0) { versionStr = versionStr[0..space]; Version version = new(versionStr); return version; } } } finally { ArrayPool<byte>.Shared.Return(bufferArray); } } return null; } } private bool FindModuleIndex(ulong baseAddr, out int index) { /* GetModuleByOffset returns the first module (from startIndex) which * includes baseAddr. * However when loading 64-bit dumps of 32-bit processes it seems that * the module sizes are sometimes wrong, which may cause a wrong module * to be found because it overlaps the beginning of the queried module, * so search until we find a module that actually has the correct * baseAddr */ int nextIndex = 0; while (true) { if (DebugSymbols.GetModuleByOffset(baseAddr, nextIndex, out index, out ulong claimedBaseAddr) < 0) { index = 0; return false; } if (claimedBaseAddr == baseAddr) return true; nextIndex = index + 1; } } public IEnumerable<uint> EnumerateOSThreadIds() { DebugSystemObjects.GetNumberThreads(out int count); if (count == 0) return Array.Empty<uint>(); uint[] result = new uint[count]; HResult hr = DebugSystemObjects.GetThreadSystemIDs(result); if (hr) return result; return Array.Empty<uint>(); } public ulong GetThreadTeb(uint osThreadId) { int curr = DebugSystemObjects.CurrentThreadId; try { HResult hr = DebugSystemObjects.GetThreadIdBySystemId((int)osThreadId, out int id); if (hr) { DebugSystemObjects.CurrentThreadId = id; hr = DebugSystemObjects.GetCurrentThreadTeb(out ulong teb); if (hr) return teb; } } finally { DebugSystemObjects.CurrentThreadId = curr; } return 0; } public unsafe bool Read<T>(ulong address, out T value) where T : unmanaged { Span<byte> buffer = stackalloc byte[sizeof(T)]; if (Read(address, buffer) == buffer.Length) { value = Unsafe.As<byte, T>(ref MemoryMarshal.GetReference(buffer)); return true; } value = default; return false; } public T Read<T>(ulong address) where T : unmanaged { Read(address, out T result); return result; } public bool ReadPointer(ulong address, out ulong value) { Span<byte> buffer = stackalloc byte[IntPtr.Size]; if (Read(address, buffer) == IntPtr.Size) { value = Unsafe.As<byte, nuint>(ref MemoryMarshal.GetReference(buffer)); return true; } value = 0; return false; } public ulong ReadPointer(ulong address) { ReadPointer(address, out ulong value); return value; } } }