// Copyright (c) 2018 Siegfried Pammer // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using System.Security.Cryptography; using System.Text; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; namespace ICSharpCode.Decompiler.DebugInfo { public class PortablePdbWriter { static readonly FileVersionInfo decompilerVersion = FileVersionInfo.GetVersionInfo(typeof(CSharpDecompiler).Assembly.Location); public static bool HasCodeViewDebugDirectoryEntry(PEFile file) { return file != null && file.Reader.ReadDebugDirectory().Any(entry => entry.Type == DebugDirectoryEntryType.CodeView); } private static bool IncludeTypeWhenGeneratingPdb(PEFile module, TypeDefinitionHandle type, DecompilerSettings settings) { var metadata = module.Metadata; var typeDef = metadata.GetTypeDefinition(type); string name = metadata.GetString(typeDef.Name); string ns = metadata.GetString(typeDef.Namespace); if (name == "" || CSharpDecompiler.MemberIsHidden(module, type, settings)) return false; if (ns == "XamlGeneratedNamespace" && name == "GeneratedInternalTypeHelper") return false; if (!typeDef.IsNested && RemoveEmbeddedAttributes.attributeNames.Contains(ns + "." + name)) return false; return true; } public static void WritePdb( PEFile file, CSharpDecompiler decompiler, DecompilerSettings settings, Stream targetStream, bool noLogo = false, BlobContentId? pdbId = null, IProgress progress = null) { MetadataBuilder metadata = new MetadataBuilder(); MetadataReader reader = file.Metadata; var entrypointHandle = MetadataTokens.MethodDefinitionHandle(file.Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress); var sequencePointBlobs = new Dictionary(); var emptyList = new List(); var localScopes = new List<(MethodDefinitionHandle Method, ImportScopeInfo Import, int Offset, int Length, HashSet Locals)>(); var stateMachineMethods = new List<(MethodDefinitionHandle MoveNextMethod, MethodDefinitionHandle KickoffMethod)>(); var customDebugInfo = new List<(EntityHandle Parent, GuidHandle Guid, BlobHandle Blob)>(); var customMethodDebugInfo = new List<(MethodDefinitionHandle Parent, GuidHandle Guid, BlobHandle Blob)>(); var globalImportScope = metadata.AddImportScope(default, default); string BuildFileNameFromTypeName(TypeDefinitionHandle handle) { var typeName = handle.GetFullTypeName(reader).TopLevelTypeName; string ns = settings.UseNestedDirectoriesForNamespaces ? WholeProjectDecompiler.CleanUpPath(typeName.Namespace) : WholeProjectDecompiler.CleanUpDirectoryName(typeName.Namespace); return Path.Combine(ns, WholeProjectDecompiler.CleanUpFileName(typeName.Name, ".cs")); } var sourceFiles = reader.GetTopLevelTypeDefinitions().Where(t => IncludeTypeWhenGeneratingPdb(file, t, settings)).GroupBy(BuildFileNameFromTypeName).ToList(); DecompilationProgress currentProgress = new() { TotalUnits = sourceFiles.Count, UnitsCompleted = 0, Title = "Generating portable PDB..." }; foreach (var sourceFile in sourceFiles) { // Generate syntax tree var syntaxTree = decompiler.DecompileTypes(sourceFile); if (progress != null) { currentProgress.UnitsCompleted++; progress.Report(currentProgress); } if (!syntaxTree.HasChildren) continue; // Generate source and checksum if (!noLogo) syntaxTree.InsertChildAfter(null, new Comment(" PDB and source generated by ICSharpCode.Decompiler " + decompilerVersion.FileVersion), Roles.Comment); var sourceText = SyntaxTreeToString(syntaxTree, settings); // Generate sequence points for the syntax tree var sequencePoints = decompiler.CreateSequencePoints(syntaxTree); // Generate other debug information var debugInfoGen = new DebugInfoGenerator(decompiler.TypeSystem); syntaxTree.AcceptVisitor(debugInfoGen); lock (metadata) { var sourceBlob = WriteSourceToBlob(metadata, sourceText, out var sourceCheckSum); var name = metadata.GetOrAddDocumentName(sourceFile.Key); // Create Document(Handle) var document = metadata.AddDocument(name, hashAlgorithm: metadata.GetOrAddGuid(KnownGuids.HashAlgorithmSHA256), hash: metadata.GetOrAddBlob(sourceCheckSum), language: metadata.GetOrAddGuid(KnownGuids.CSharpLanguageGuid)); // Add embedded source to the PDB customDebugInfo.Add((document, metadata.GetOrAddGuid(KnownGuids.EmbeddedSource), sourceBlob)); debugInfoGen.GenerateImportScopes(metadata, globalImportScope); localScopes.AddRange(debugInfoGen.LocalScopes); foreach (var function in debugInfoGen.Functions) { var method = function.MoveNextMethod ?? function.Method; var methodHandle = (MethodDefinitionHandle)method.MetadataToken; sequencePoints.TryGetValue(function, out var points); ProcessMethod(methodHandle, document, points, syntaxTree); if (function.MoveNextMethod != null) { stateMachineMethods.Add(( (MethodDefinitionHandle)function.MoveNextMethod.MetadataToken, (MethodDefinitionHandle)function.Method.MetadataToken )); customDebugInfo.Add(( function.MoveNextMethod.MetadataToken, metadata.GetOrAddGuid(KnownGuids.StateMachineHoistedLocalScopes), metadata.GetOrAddBlob(BuildStateMachineHoistedLocalScopes(function)) )); } if (function.IsAsync) { customMethodDebugInfo.Add((methodHandle, metadata.GetOrAddGuid(KnownGuids.MethodSteppingInformation), metadata.GetOrAddBlob(function.AsyncDebugInfo.BuildBlob(methodHandle)))); } } } } foreach (var method in reader.MethodDefinitions) { var md = reader.GetMethodDefinition(method); if (sequencePointBlobs.TryGetValue(method, out var info)) { metadata.AddMethodDebugInformation(info.Document, info.SequencePoints); } else { metadata.AddMethodDebugInformation(default, default); } } localScopes.Sort((x, y) => { if (x.Method != y.Method) { return MetadataTokens.GetRowNumber(x.Method) - MetadataTokens.GetRowNumber(y.Method); } if (x.Offset != y.Offset) { return x.Offset - y.Offset; } return y.Length - x.Length; }); foreach (var localScope in localScopes) { int nextRow = metadata.GetRowCount(TableIndex.LocalVariable) + 1; var firstLocalVariable = MetadataTokens.LocalVariableHandle(nextRow); foreach (var local in localScope.Locals.OrderBy(l => l.Index)) { var localVarName = local.Name != null ? metadata.GetOrAddString(local.Name) : default; metadata.AddLocalVariable(LocalVariableAttributes.None, local.Index.Value, localVarName); } metadata.AddLocalScope(localScope.Method, localScope.Import.Handle, firstLocalVariable, default, localScope.Offset, localScope.Length); } stateMachineMethods.SortBy(row => MetadataTokens.GetRowNumber(row.MoveNextMethod)); foreach (var row in stateMachineMethods) { metadata.AddStateMachineMethod(row.MoveNextMethod, row.KickoffMethod); } customMethodDebugInfo.SortBy(row => MetadataTokens.GetRowNumber(row.Parent)); foreach (var row in customMethodDebugInfo) { metadata.AddCustomDebugInformation(row.Parent, row.Guid, row.Blob); } customDebugInfo.SortBy(row => MetadataTokens.GetRowNumber(row.Parent)); foreach (var row in customDebugInfo) { metadata.AddCustomDebugInformation(row.Parent, row.Guid, row.Blob); } if (pdbId == null) { var debugDir = file.Reader.ReadDebugDirectory().FirstOrDefault(dir => dir.Type == DebugDirectoryEntryType.CodeView); var portable = file.Reader.ReadCodeViewDebugDirectoryData(debugDir); pdbId = new BlobContentId(portable.Guid, debugDir.Stamp); } PortablePdbBuilder serializer = new PortablePdbBuilder(metadata, GetRowCounts(reader), entrypointHandle, blobs => pdbId.Value); BlobBuilder blobBuilder = new BlobBuilder(); serializer.Serialize(blobBuilder); blobBuilder.WriteContentTo(targetStream); void ProcessMethod(MethodDefinitionHandle method, DocumentHandle document, List sequencePoints, SyntaxTree syntaxTree) { var methodDef = reader.GetMethodDefinition(method); int localSignatureRowId; MethodBodyBlock methodBody; if (methodDef.RelativeVirtualAddress != 0) { methodBody = file.Reader.GetMethodBody(methodDef.RelativeVirtualAddress); localSignatureRowId = methodBody.LocalSignature.IsNil ? 0 : MetadataTokens.GetRowNumber(methodBody.LocalSignature); } else { methodBody = null; localSignatureRowId = 0; } // Check if sequence points were already processed - ILFunction gets defined in C# twice: // This may happen if a compiler-generated function gets transformed into a lambda expression, // but its method definition is not removed from the syntax tree. if (!sequencePointBlobs.ContainsKey(method)) { if (sequencePoints?.Count > 0) sequencePointBlobs.Add(method, (document, EncodeSequencePoints(metadata, localSignatureRowId, sequencePoints))); else sequencePointBlobs.Add(method, (default, default)); } else { Debug.Assert(false, "Duplicate sequence point definition detected: " + MetadataTokens.GetToken(method).ToString("X8")); } } } static BlobBuilder BuildStateMachineHoistedLocalScopes(ILFunction function) { var builder = new BlobBuilder(); foreach (var variable in function.Variables.Where(v => v.StateMachineField != null).OrderBy(v => MetadataTokens.GetRowNumber(v.StateMachineField.MetadataToken))) { builder.WriteUInt32(0); builder.WriteUInt32((uint)function.CodeSize); } return builder; } static BlobHandle WriteSourceToBlob(MetadataBuilder metadata, string sourceText, out byte[] sourceCheckSum) { var builder = new BlobBuilder(); using (var memory = new MemoryStream()) { var deflate = new DeflateStream(memory, CompressionLevel.Optimal, leaveOpen: true); byte[] bytes = Encoding.UTF8.GetBytes(sourceText); deflate.Write(bytes, 0, bytes.Length); deflate.Close(); byte[] buffer = memory.ToArray(); builder.WriteInt32(bytes.Length); // compressed builder.WriteBytes(buffer); using (var hasher = SHA256.Create()) { sourceCheckSum = hasher.ComputeHash(bytes); } } return metadata.GetOrAddBlob(builder); } static BlobHandle EncodeSequencePoints(MetadataBuilder metadata, int localSignatureRowId, List sequencePoints) { if (sequencePoints.Count == 0) return default; var writer = new BlobBuilder(); // header: writer.WriteCompressedInteger(localSignatureRowId); int previousOffset = -1; int previousStartLine = -1; int previousStartColumn = -1; for (int i = 0; i < sequencePoints.Count; i++) { var sequencePoint = sequencePoints[i]; // delta IL offset: if (i > 0) writer.WriteCompressedInteger(sequencePoint.Offset - previousOffset); else writer.WriteCompressedInteger(sequencePoint.Offset); previousOffset = sequencePoint.Offset; if (sequencePoint.IsHidden) { writer.WriteInt16(0); continue; } int lineDelta = sequencePoint.EndLine - sequencePoint.StartLine; int columnDelta = sequencePoint.EndColumn - sequencePoint.StartColumn; writer.WriteCompressedInteger(lineDelta); if (lineDelta == 0) { writer.WriteCompressedInteger(columnDelta); } else { writer.WriteCompressedSignedInteger(columnDelta); } if (previousStartLine < 0) { writer.WriteCompressedInteger(sequencePoint.StartLine); writer.WriteCompressedInteger(sequencePoint.StartColumn); } else { writer.WriteCompressedSignedInteger(sequencePoint.StartLine - previousStartLine); writer.WriteCompressedSignedInteger(sequencePoint.StartColumn - previousStartColumn); } previousStartLine = sequencePoint.StartLine; previousStartColumn = sequencePoint.StartColumn; } return metadata.GetOrAddBlob(writer); } static ImmutableArray GetRowCounts(MetadataReader reader) { var builder = ImmutableArray.CreateBuilder(MetadataTokens.TableCount); for (int i = 0; i < MetadataTokens.TableCount; i++) { builder.Add(reader.GetTableRowCount((TableIndex)i)); } return builder.MoveToImmutable(); } static string SyntaxTreeToString(SyntaxTree syntaxTree, DecompilerSettings settings) { StringWriter w = new StringWriter(); TokenWriter tokenWriter = new TextWriterTokenWriter(w); tokenWriter = TokenWriter.WrapInWriterThatSetsLocationsInAST(tokenWriter); syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); return w.ToString(); } } }