From: Igor Kulaychuk Date: Mon, 13 Nov 2017 19:19:47 +0000 (+0300) Subject: Add SymbolReader.cs from coreclr repo X-Git-Tag: submit/tizen/20180620.071641~255 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=2d373fefe9bb2915d2ba70a9d6bf841f15ce5aba;p=sdk%2Ftools%2Fnetcoredbg.git Add SymbolReader.cs from coreclr repo --- diff --git a/src/ToolBox/SOS/NETCore/SymbolReader.cs b/src/ToolBox/SOS/NETCore/SymbolReader.cs new file mode 100644 index 0000000..7a4bb52 --- /dev/null +++ b/src/ToolBox/SOS/NETCore/SymbolReader.cs @@ -0,0 +1,782 @@ +// 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, CharSet = CharSet.Unicode)] + internal struct LocalVarInfo + { + public int startOffset; + public int endOffset; + public string name; + } + + [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(); + if (methodDebugHandle.IsNil) + return false; + + 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; + LocalVarInfo info = new LocalVarInfo(); + info.startOffset = scope.StartOffset; + info.endOffset = scope.EndOffset; + info.name = openedReader.Reader.GetString(localVar.Name); + locals.Add(info); + } + } + } + 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); + } + + structSize = Marshal.SizeOf(); + + debugInfo.localsSize = locals.Count; + ptr = debugInfo.locals; + + foreach (var info in locals) + { + Marshal.StructureToPtr(info, ptr, false); + ptr = (IntPtr)(ptr.ToInt64() + structSize); + } + + 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 = 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; + } + } + } +}