// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; namespace SOS { internal class SymbolReader { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct DebugInfo { public int lineNumber; public int ilOffset; public string fileName; } [StructLayout(LayoutKind.Sequential)] internal struct MethodDebugInfo { public IntPtr points; public int size; public IntPtr locals; public int localsSize; } /// /// Read memory callback /// /// number of bytes read or 0 for error internal unsafe delegate int ReadMemoryDelegate(ulong address, byte* buffer, int count); private sealed class OpenedReader : IDisposable { public readonly MetadataReaderProvider Provider; public readonly MetadataReader Reader; public OpenedReader(MetadataReaderProvider provider, MetadataReader reader) { Debug.Assert(provider != null); Debug.Assert(reader != null); Provider = provider; Reader = reader; } public void Dispose() => Provider.Dispose(); } /// /// Stream implementation to read debugger target memory for in-memory PDBs /// private class TargetStream : Stream { readonly ulong _address; readonly ReadMemoryDelegate _readMemory; public override long Position { get; set; } public override long Length { get; } public override bool CanSeek { get { return true; } } public override bool CanRead { get { return true; } } public override bool CanWrite { get { return false; } } public TargetStream(ulong address, int size, ReadMemoryDelegate readMemory) : base() { _address = address; _readMemory = readMemory; Length = size; Position = 0; } public override int Read(byte[] buffer, int offset, int count) { if (Position + count > Length) { throw new ArgumentOutOfRangeException(); } unsafe { fixed (byte* p = &buffer[offset]) { int read = _readMemory(_address + (ulong)Position, p, count); Position += read; return read; } } } public override long Seek(long offset, SeekOrigin origin) { switch (origin) { case SeekOrigin.Begin: Position = offset; break; case SeekOrigin.End: Position = Length + offset; break; case SeekOrigin.Current: Position += offset; break; } return Position; } public override void Flush() { } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } } /// /// Quick fix for Path.GetFileName which incorrectly handles Windows-style paths on Linux /// /// File path to be processed /// Last component of path private static string GetFileName(string pathName) { int pos = pathName.LastIndexOfAny(new char[] { '/', '\\'}); if (pos < 0) return pathName; return pathName.Substring(pos + 1); } /// /// Checks availability of debugging information for given assembly. /// /// /// File path of the assembly or null if the module is in-memory or dynamic (generated by Reflection.Emit) /// /// type of in-memory PE layout, if true, file based layout otherwise, loaded layout /// /// Loaded PE image address or zero if the module is dynamic (generated by Reflection.Emit). /// Dynamic modules have their PDBs (if any) generated to an in-memory stream /// (pointed to by and ). /// /// loaded PE image size /// in memory PDB address or zero /// in memory PDB size /// Symbol reader handle or zero if error internal static IntPtr LoadSymbolsForModule(string assemblyPath, bool isFileLayout, ulong loadedPeAddress, int loadedPeSize, ulong inMemoryPdbAddress, int inMemoryPdbSize, ReadMemoryDelegate readMemory) { try { TargetStream peStream = null; if (assemblyPath == null && loadedPeAddress != 0) { peStream = new TargetStream(loadedPeAddress, loadedPeSize, readMemory); } TargetStream pdbStream = null; if (inMemoryPdbAddress != 0) { pdbStream = new TargetStream(inMemoryPdbAddress, inMemoryPdbSize, readMemory); } OpenedReader openedReader = GetReader(assemblyPath, isFileLayout, peStream, pdbStream); if (openedReader != null) { GCHandle gch = GCHandle.Alloc(openedReader); return GCHandle.ToIntPtr(gch); } } catch { } return IntPtr.Zero; } /// /// Cleanup and dispose of symbol reader handle /// /// symbol reader handle returned by LoadSymbolsForModule internal static void Dispose(IntPtr symbolReaderHandle) { Debug.Assert(symbolReaderHandle != IntPtr.Zero); try { GCHandle gch = GCHandle.FromIntPtr(symbolReaderHandle); ((OpenedReader)gch.Target).Dispose(); gch.Free(); } catch { } } /// /// Returns method token and IL offset for given source line number. /// /// symbol reader handle returned by LoadSymbolsForModule /// source file name and path /// source line number /// method token return /// IL offset return /// true if information is available internal static bool ResolveSequencePoint(IntPtr symbolReaderHandle, string filePath, int lineNumber, out int methodToken, out int ilOffset) { Debug.Assert(symbolReaderHandle != IntPtr.Zero); methodToken = 0; ilOffset = 0; GCHandle gch = GCHandle.FromIntPtr(symbolReaderHandle); MetadataReader reader = ((OpenedReader)gch.Target).Reader; try { string fileName = GetFileName(filePath); foreach (MethodDebugInformationHandle methodDebugInformationHandle in reader.MethodDebugInformation) { MethodDebugInformation methodDebugInfo = reader.GetMethodDebugInformation(methodDebugInformationHandle); SequencePointCollection sequencePoints = methodDebugInfo.GetSequencePoints(); foreach (SequencePoint point in sequencePoints) { string sourceName = reader.GetString(reader.GetDocument(point.Document).Name); if (point.StartLine == lineNumber && GetFileName(sourceName) == fileName) { methodToken = MetadataTokens.GetToken(methodDebugInformationHandle.ToDefinitionHandle()); ilOffset = point.Offset; return true; } } } } catch { } return false; } /// /// Returns source line number and source file name for given IL offset and method token. /// /// symbol reader handle returned by LoadSymbolsForModule /// method token /// IL offset /// source line number return /// source file name return /// true if information is available internal static bool GetLineByILOffset(IntPtr symbolReaderHandle, int methodToken, long ilOffset, out int lineNumber, out IntPtr fileName) { lineNumber = 0; fileName = IntPtr.Zero; string sourceFileName = null; if (!GetSourceLineByILOffset(symbolReaderHandle, methodToken, ilOffset, out lineNumber, out sourceFileName)) { return false; } fileName = Marshal.StringToBSTR(sourceFileName); sourceFileName = null; return true; } /// /// Helper method to return source line number and source file name for given IL offset and method token. /// /// symbol reader handle returned by LoadSymbolsForModule /// method token /// IL offset /// source line number return /// source file name return /// true if information is available private static bool GetSourceLineByILOffset(IntPtr symbolReaderHandle, int methodToken, long ilOffset, out int lineNumber, out string fileName) { Debug.Assert(symbolReaderHandle != IntPtr.Zero); lineNumber = 0; fileName = null; GCHandle gch = GCHandle.FromIntPtr(symbolReaderHandle); MetadataReader reader = ((OpenedReader)gch.Target).Reader; try { Handle handle = MetadataTokens.Handle(methodToken); if (handle.Kind != HandleKind.MethodDefinition) return false; MethodDebugInformationHandle methodDebugHandle = ((MethodDefinitionHandle)handle).ToDebugInformationHandle(); MethodDebugInformation methodDebugInfo = reader.GetMethodDebugInformation(methodDebugHandle); SequencePointCollection sequencePoints = methodDebugInfo.GetSequencePoints(); SequencePoint nearestPoint = sequencePoints.GetEnumerator().Current; foreach (SequencePoint point in sequencePoints) { if (point.Offset < ilOffset) { nearestPoint = point; } else { if (point.Offset == ilOffset) nearestPoint = point; if (nearestPoint.StartLine == 0 || nearestPoint.StartLine == SequencePoint.HiddenLine) return false; lineNumber = nearestPoint.StartLine; fileName = reader.GetString(reader.GetDocument(nearestPoint.Document).Name); return true; } } } catch { } return false; } /// /// Returns local variable name for given local index and IL offset. /// /// symbol reader handle returned by LoadSymbolsForModule /// method token /// local variable index /// local variable name return /// true if name has been found internal static bool GetLocalVariableName(IntPtr symbolReaderHandle, int methodToken, int localIndex, out IntPtr localVarName) { localVarName = IntPtr.Zero; string localVar = null; if (!GetLocalVariableByIndex(symbolReaderHandle, methodToken, localIndex, out localVar)) return false; localVarName = Marshal.StringToBSTR(localVar); localVar = null; return true; } /// /// Helper method to return local variable name for given local index and IL offset. /// /// symbol reader handle returned by LoadSymbolsForModule /// method token /// local variable index /// local variable name return /// true if name has been found internal static bool GetLocalVariableByIndex(IntPtr symbolReaderHandle, int methodToken, int localIndex, out string localVarName) { Debug.Assert(symbolReaderHandle != IntPtr.Zero); localVarName = null; GCHandle gch = GCHandle.FromIntPtr(symbolReaderHandle); MetadataReader reader = ((OpenedReader)gch.Target).Reader; try { Handle handle = MetadataTokens.Handle(methodToken); if (handle.Kind != HandleKind.MethodDefinition) return false; MethodDebugInformationHandle methodDebugHandle = ((MethodDefinitionHandle)handle).ToDebugInformationHandle(); LocalScopeHandleCollection localScopes = reader.GetLocalScopes(methodDebugHandle); foreach (LocalScopeHandle scopeHandle in localScopes) { LocalScope scope = reader.GetLocalScope(scopeHandle); LocalVariableHandleCollection localVars = scope.GetLocalVariables(); foreach (LocalVariableHandle varHandle in localVars) { LocalVariable localVar = reader.GetLocalVariable(varHandle); if (localVar.Index == localIndex) { if (localVar.Attributes == LocalVariableAttributes.DebuggerHidden) return false; localVarName = reader.GetString(localVar.Name); return true; } } } } catch { } return false; } internal static bool GetLocalsInfoForMethod(string assemblyPath, int methodToken, out List locals) { locals = null; OpenedReader openedReader = GetReader(assemblyPath, isFileLayout: true, peStream: null, pdbStream: null); if (openedReader == null) return false; using (openedReader) { try { Handle handle = MetadataTokens.Handle(methodToken); if (handle.Kind != HandleKind.MethodDefinition) return false; locals = new List(); MethodDebugInformationHandle methodDebugHandle = ((MethodDefinitionHandle)handle).ToDebugInformationHandle(); LocalScopeHandleCollection localScopes = openedReader.Reader.GetLocalScopes(methodDebugHandle); foreach (LocalScopeHandle scopeHandle in localScopes) { LocalScope scope = openedReader.Reader.GetLocalScope(scopeHandle); LocalVariableHandleCollection localVars = scope.GetLocalVariables(); foreach (LocalVariableHandle varHandle in localVars) { LocalVariable localVar = openedReader.Reader.GetLocalVariable(varHandle); if (localVar.Attributes == LocalVariableAttributes.DebuggerHidden) continue; locals.Add(openedReader.Reader.GetString(localVar.Name)); } } } catch { return false; } } return true; } /// /// Returns source name, line numbers and IL offsets for given method token. /// /// file path of the assembly /// method token /// structure with debug information return /// true if information is available /// used by the gdb JIT support (not SOS). Does not support in-memory PEs or PDBs internal static bool GetInfoForMethod(string assemblyPath, int methodToken, ref MethodDebugInfo debugInfo) { try { List points = null; List locals = null; if (!GetDebugInfoForMethod(assemblyPath, methodToken, out points)) { return false; } if (!GetLocalsInfoForMethod(assemblyPath, methodToken, out locals)) { return false; } var structSize = Marshal.SizeOf(); debugInfo.size = points.Count; var ptr = debugInfo.points; foreach (var info in points) { Marshal.StructureToPtr(info, ptr, false); ptr = (IntPtr)(ptr.ToInt64() + structSize); } debugInfo.localsSize = locals.Count; debugInfo.locals = Marshal.AllocHGlobal(debugInfo.localsSize * Marshal.SizeOf()); IntPtr ptrLocals = debugInfo.locals; foreach (string s in locals) { Marshal.WriteIntPtr(ptrLocals, Marshal.StringToHGlobalUni(s)); ptrLocals += Marshal.SizeOf(); } return true; } catch { } return false; } /// /// Helper method to return source name, line numbers and IL offsets for given method token. /// /// file path of the assembly /// method token /// list of debug information for each sequence point return /// true if information is available /// used by the gdb JIT support (not SOS). Does not support in-memory PEs or PDBs private static bool GetDebugInfoForMethod(string assemblyPath, int methodToken, out List points) { points = null; OpenedReader openedReader = GetReader(assemblyPath, isFileLayout: true, peStream: null, pdbStream: null); if (openedReader == null) return false; using (openedReader) { try { Handle handle = MetadataTokens.Handle(methodToken); if (handle.Kind != HandleKind.MethodDefinition) return false; points = new List(); MethodDebugInformationHandle methodDebugHandle = ((MethodDefinitionHandle)handle).ToDebugInformationHandle(); MethodDebugInformation methodDebugInfo = openedReader.Reader.GetMethodDebugInformation(methodDebugHandle); SequencePointCollection sequencePoints = methodDebugInfo.GetSequencePoints(); foreach (SequencePoint point in sequencePoints) { DebugInfo debugInfo = new DebugInfo(); debugInfo.lineNumber = point.StartLine; debugInfo.fileName = GetFileName(openedReader.Reader.GetString(openedReader.Reader.GetDocument(point.Document).Name)); debugInfo.ilOffset = point.Offset; points.Add(debugInfo); } } catch { return false; } } return true; } /// /// Returns the portable PDB reader for the assembly path /// /// file path of the assembly or null if the module is in-memory or dynamic /// type of in-memory PE layout, if true, file based layout otherwise, loaded layout /// optional in-memory PE stream /// optional in-memory PDB stream /// reader/provider wrapper instance /// /// Assumes that neither PE image nor PDB loaded into memory can be unloaded or moved around. /// private static OpenedReader GetReader(string assemblyPath, bool isFileLayout, Stream peStream, Stream pdbStream) { return (pdbStream != null) ? TryOpenReaderForInMemoryPdb(pdbStream) : TryOpenReaderFromAssembly(assemblyPath, isFileLayout, peStream); } private static OpenedReader TryOpenReaderForInMemoryPdb(Stream pdbStream) { Debug.Assert(pdbStream != null); byte[] buffer = new byte[sizeof(uint)]; if (pdbStream.Read(buffer, 0, sizeof(uint)) != sizeof(uint)) { return null; } uint signature = BitConverter.ToUInt32(buffer, 0); // quick check to avoid throwing exceptions below in common cases: const uint ManagedMetadataSignature = 0x424A5342; if (signature != ManagedMetadataSignature) { // not a Portable PDB return null; } OpenedReader result = null; MetadataReaderProvider provider = null; try { pdbStream.Position = 0; provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); result = new OpenedReader(provider, provider.GetMetadataReader()); } catch (Exception e) when (e is BadImageFormatException || e is IOException) { return null; } finally { if (result == null) { provider?.Dispose(); } } return result; } private static OpenedReader TryOpenReaderFromAssembly(string assemblyPath, bool isFileLayout, Stream peStream) { if (assemblyPath == null && peStream == null) return null; PEStreamOptions options = isFileLayout ? PEStreamOptions.Default : PEStreamOptions.IsLoadedImage; if (peStream == null) { peStream = TryOpenFile(assemblyPath); if (peStream == null) return null; options = PEStreamOptions.Default; } try { using (var peReader = new PEReader(peStream, options)) { DebugDirectoryEntry codeViewEntry, embeddedPdbEntry; ReadPortableDebugTableEntries(peReader, out codeViewEntry, out embeddedPdbEntry); // First try .pdb file specified in CodeView data (we prefer .pdb file on disk over embedded PDB // since embedded PDB needs decompression which is less efficient than memory-mapping the file). if (codeViewEntry.DataSize != 0) { var result = TryOpenReaderFromCodeView(peReader, codeViewEntry, assemblyPath); if (result != null) { return result; } } // if it failed try Embedded Portable PDB (if available): if (embeddedPdbEntry.DataSize != 0) { return TryOpenReaderFromEmbeddedPdb(peReader, embeddedPdbEntry); } } } catch (Exception e) when (e is BadImageFormatException || e is IOException) { // nop } return null; } private static void ReadPortableDebugTableEntries(PEReader peReader, out DebugDirectoryEntry codeViewEntry, out DebugDirectoryEntry embeddedPdbEntry) { // See spec: https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PE-COFF.md codeViewEntry = default(DebugDirectoryEntry); embeddedPdbEntry = default(DebugDirectoryEntry); foreach (DebugDirectoryEntry entry in peReader.ReadDebugDirectory()) { if (entry.Type == DebugDirectoryEntryType.CodeView) { const ushort PortableCodeViewVersionMagic = 0x504d; if (entry.MinorVersion != PortableCodeViewVersionMagic) { continue; } codeViewEntry = entry; } else if (entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb) { embeddedPdbEntry = entry; } } } private static OpenedReader TryOpenReaderFromCodeView(PEReader peReader, DebugDirectoryEntry codeViewEntry, string assemblyPath) { OpenedReader result = null; MetadataReaderProvider provider = null; try { var data = peReader.ReadCodeViewDebugDirectoryData(codeViewEntry); string pdbPath = data.Path; if (assemblyPath != null) { try { pdbPath = Path.Combine(Path.GetDirectoryName(assemblyPath), GetFileName(pdbPath)); } catch { // invalid characters in CodeView path return null; } } var pdbStream = TryOpenFile(pdbPath); if (pdbStream == null) { return null; } provider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); var reader = provider.GetMetadataReader(); // Validate that the PDB matches the assembly version if (data.Age == 1 && new BlobContentId(reader.DebugMetadataHeader.Id) == new BlobContentId(data.Guid, codeViewEntry.Stamp)) { result = new OpenedReader(provider, reader); } } catch (Exception e) when (e is BadImageFormatException || e is IOException) { return null; } finally { if (result == null) { provider?.Dispose(); } } return result; } private static OpenedReader TryOpenReaderFromEmbeddedPdb(PEReader peReader, DebugDirectoryEntry embeddedPdbEntry) { OpenedReader result = null; MetadataReaderProvider provider = null; try { // TODO: We might want to cache this provider globally (across stack traces), // since decompressing embedded PDB takes some time. provider = peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry); result = new OpenedReader(provider, provider.GetMetadataReader()); } catch (Exception e) when (e is BadImageFormatException || e is IOException) { return null; } finally { if (result == null) { provider?.Dispose(); } } return result; } private static Stream TryOpenFile(string path) { if (!File.Exists(path)) { return null; } try { return File.OpenRead(path); } catch { return null; } } } }