// dnlib: See LICENSE.txt for more info using System; using System.Collections.Generic; using System.Diagnostics; using dnlib.DotNet.Emit; using dnlib.DotNet.MD; using dnlib.DotNet.Pdb.Symbols; using dnlib.Threading; namespace dnlib.DotNet.Pdb { /// /// PDB state for a /// public sealed class PdbState { readonly SymbolReader reader; readonly Dictionary docDict = new Dictionary(); internal readonly Dictionary tokenToDocument = new Dictionary(); MethodDef userEntryPoint; readonly Compiler compiler; readonly PdbFileKind originalPdbFileKind; #if THREAD_SAFE readonly Lock theLock = Lock.Create(); #endif /// /// Gets/sets the PDB file kind. You can change it from portable PDB to embedded portable PDB /// and vice versa. Converting a Windows PDB to a portable PDB isn't supported. /// public PdbFileKind PdbFileKind { get; set; } /// /// Gets/sets the user entry point method. /// public MethodDef UserEntryPoint { get => userEntryPoint; set => userEntryPoint = value; } /// /// Gets all PDB documents /// public IEnumerable Documents { get { #if THREAD_SAFE theLock.EnterWriteLock(); try { return new List(docDict.Values); } finally { theLock.ExitWriteLock(); } #else return docDict.Values; #endif } } /// /// true if is not empty /// public bool HasDocuments { get { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif return docDict.Count > 0; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } } /// /// Constructor /// /// Module /// PDB file kind public PdbState(ModuleDef module, PdbFileKind pdbFileKind) { if (module is null) throw new ArgumentNullException(nameof(module)); compiler = CalculateCompiler(module); PdbFileKind = pdbFileKind; originalPdbFileKind = pdbFileKind; } /// /// Constructor /// /// A instance /// Owner module public PdbState(SymbolReader reader, ModuleDefMD module) { if (module is null) throw new ArgumentNullException(nameof(module)); this.reader = reader ?? throw new ArgumentNullException(nameof(reader)); reader.Initialize(module); PdbFileKind = reader.PdbFileKind; originalPdbFileKind = reader.PdbFileKind; compiler = CalculateCompiler(module); userEntryPoint = module.ResolveToken(reader.UserEntryPoint) as MethodDef; var documents = reader.Documents; int count = documents.Count; for (int i = 0; i < count; i++) Add_NoLock(documents[i]); } /// /// Adds /// /// New document /// if it wasn't inserted, or the already existing document /// if it was already inserted. public PdbDocument Add(PdbDocument doc) { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif return Add_NoLock(doc); #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } PdbDocument Add_NoLock(PdbDocument doc) { if (docDict.TryGetValue(doc, out var orig)) return orig; docDict.Add(doc, doc); if (doc.MDToken.HasValue) tokenToDocument.Add(doc.MDToken.Value, doc); return doc; } PdbDocument Add_NoLock(SymbolDocument symDoc) { var doc = PdbDocument.CreatePartialForCompare(symDoc); if (docDict.TryGetValue(doc, out var orig)) return orig; // Expensive part, can read source code etc doc.Initialize(symDoc); docDict.Add(doc, doc); if (symDoc.MDToken.HasValue) tokenToDocument.Add(symDoc.MDToken.Value, doc); return doc; } /// /// Removes /// /// Document /// true if it was removed, false if it wasn't inserted. public bool Remove(PdbDocument doc) { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif if (doc.MDToken.HasValue) tokenToDocument.Remove(doc.MDToken.Value); return docDict.Remove(doc); #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } /// /// Returns an inserted instance or null if it's not been /// inserted yet. /// /// A PDB document /// The existing or null if it doesn't exist. public PdbDocument GetExisting(PdbDocument doc) { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif docDict.TryGetValue(doc, out var orig); return orig; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } /// /// Removes all documents /// /// public void RemoveAllDocuments() => RemoveAllDocuments(false); /// /// Removes all documents and optionally returns them /// /// true if all the original s /// should be returned. /// All s if is true /// or null if is false. public List RemoveAllDocuments(bool returnDocs) { #if THREAD_SAFE theLock.EnterWriteLock(); try { #endif var docs = returnDocs ? new List(docDict.Values) : null; tokenToDocument.Clear(); docDict.Clear(); return docs; #if THREAD_SAFE } finally { theLock.ExitWriteLock(); } #endif } internal Compiler Compiler => compiler; internal void InitializeMethodBody(ModuleDefMD module, MethodDef ownerMethod, CilBody body) { if (reader is null) return; var method = reader.GetMethod(ownerMethod, 1); if (method is not null) { var pdbMethod = new PdbMethod(); pdbMethod.Scope = CreateScope(module, GenericParamContext.Create(ownerMethod), body, method.RootScope); AddSequencePoints(body, method); body.PdbMethod = pdbMethod; } } internal void InitializeCustomDebugInfos(MethodDef ownerMethod, CilBody body, IList customDebugInfos) { if (reader is null) return; var method = reader.GetMethod(ownerMethod, 1); if (method is not null) method.GetCustomDebugInfos(ownerMethod, body, customDebugInfos); } static Compiler CalculateCompiler(ModuleDef module) { if (module is null) return Compiler.Other; foreach (var asmRef in module.GetAssemblyRefs()) { if (asmRef.Name == nameAssemblyVisualBasic || asmRef.Name == nameAssemblyVisualBasicCore) return Compiler.VisualBasic; } // Disable this for now, we shouldn't be resolving types this early since we could be called by the ModuleDefMD ctor #if false // The VB runtime can also be embedded, and if so, it seems that "Microsoft.VisualBasic.Embedded" // attribute is added to the assembly's custom attributes. var asm = module.Assembly; if (asm is not null && asm.CustomAttributes.IsDefined("Microsoft.VisualBasic.Embedded")) return Compiler.VisualBasic; #endif return Compiler.Other; } static readonly UTF8String nameAssemblyVisualBasic = new UTF8String("Microsoft.VisualBasic"); // .NET Core 3.0 has this assembly because Microsoft.VisualBasic contains WinForms refs static readonly UTF8String nameAssemblyVisualBasicCore = new UTF8String("Microsoft.VisualBasic.Core"); void AddSequencePoints(CilBody body, SymbolMethod method) { int instrIndex = 0; var sequencePoints = method.SequencePoints; int count = sequencePoints.Count; for (int i = 0; i < count; i++) { var sp = sequencePoints[i]; var instr = GetInstruction(body.Instructions, sp.Offset, ref instrIndex); if (instr is null) continue; var seqPoint = new SequencePoint() { Document = Add_NoLock(sp.Document), StartLine = sp.Line, StartColumn = sp.Column, EndLine = sp.EndLine, EndColumn = sp.EndColumn, }; instr.SequencePoint = seqPoint; } } struct CreateScopeState { public SymbolScope SymScope; public PdbScope PdbScope; public IList Children; public int ChildrenIndex; } PdbScope CreateScope(ModuleDefMD module, GenericParamContext gpContext, CilBody body, SymbolScope symScope) { if (symScope is null) return null; // Don't use recursive calls var stack = new Stack(); var state = new CreateScopeState() { SymScope = symScope }; int endIsInclusiveValue = PdbUtils.IsEndInclusive(originalPdbFileKind, Compiler) ? 1 : 0; recursive_call: int instrIndex = 0; state.PdbScope = new PdbScope() { Start = GetInstruction(body.Instructions, state.SymScope.StartOffset, ref instrIndex), End = GetInstruction(body.Instructions, state.SymScope.EndOffset + endIsInclusiveValue, ref instrIndex), }; var cdis = state.SymScope.CustomDebugInfos; int count = cdis.Count; for (int i = 0; i < count; i++) state.PdbScope.CustomDebugInfos.Add(cdis[i]); var locals = state.SymScope.Locals; count = locals.Count; for (int i = 0; i < count; i++) { var symLocal = locals[i]; int localIndex = symLocal.Index; if ((uint)localIndex >= (uint)body.Variables.Count) { // VB sometimes creates a PDB local without a metadata local continue; } var local = body.Variables[localIndex]; var name = symLocal.Name; local.SetName(name); var attributes = symLocal.Attributes; local.SetAttributes(attributes); var pdbLocal = new PdbLocal(local, name, attributes); cdis = symLocal.CustomDebugInfos; int count2 = cdis.Count; for (int j = 0; j < count2; j++) pdbLocal.CustomDebugInfos.Add(cdis[j]); state.PdbScope.Variables.Add(pdbLocal); } var namespaces = state.SymScope.Namespaces; count = namespaces.Count; for (int i = 0; i < count; i++) state.PdbScope.Namespaces.Add(namespaces[i].Name); state.PdbScope.ImportScope = state.SymScope.ImportScope; var constants = state.SymScope.GetConstants(module, gpContext); for (int i = 0; i < constants.Count; i++) { var constant = constants[i]; var type = constant.Type.RemovePinnedAndModifiers(); if (type is not null) { // Fix a few values since they're stored as some other type in the PDB switch (type.ElementType) { case ElementType.Boolean: if (constant.Value is short) constant.Value = (short)constant.Value != 0; break; case ElementType.Char: if (constant.Value is ushort) constant.Value = (char)(ushort)constant.Value; break; case ElementType.I1: if (constant.Value is short) constant.Value = (sbyte)(short)constant.Value; break; case ElementType.U1: if (constant.Value is short) constant.Value = (byte)(short)constant.Value; break; case ElementType.I2: case ElementType.U2: case ElementType.I4: case ElementType.U4: case ElementType.I8: case ElementType.U8: case ElementType.R4: case ElementType.R8: case ElementType.Void: case ElementType.Ptr: case ElementType.ByRef: case ElementType.TypedByRef: case ElementType.I: case ElementType.U: case ElementType.FnPtr: case ElementType.ValueType: break; case ElementType.String: if (PdbFileKind == PdbFileKind.WindowsPDB) { // "" is stored as null, and null is stored as (int)0 if (constant.Value is int && (int)constant.Value == 0) constant.Value = null; else if (constant.Value is null) constant.Value = string.Empty; } else Debug.Assert(PdbFileKind == PdbFileKind.PortablePDB || PdbFileKind == PdbFileKind.EmbeddedPortablePDB); break; case ElementType.Object: case ElementType.Class: case ElementType.SZArray: case ElementType.Array: default: if (constant.Value is int && (int)constant.Value == 0) constant.Value = null; break; case ElementType.GenericInst: var gis = (GenericInstSig)type; if (gis.GenericType is ValueTypeSig) break; goto case ElementType.Class; case ElementType.Var: case ElementType.MVar: var gp = ((GenericSig)type).GenericParam; if (gp is not null) { if (gp.HasNotNullableValueTypeConstraint) break; if (gp.HasReferenceTypeConstraint) goto case ElementType.Class; } break; } } state.PdbScope.Constants.Add(constant); } // Here's the now somewhat obfuscated for loop state.ChildrenIndex = 0; state.Children = state.SymScope.Children; do_return: if (state.ChildrenIndex < state.Children.Count) { var child = state.Children[state.ChildrenIndex]; stack.Push(state); state = new CreateScopeState() { SymScope = child }; goto recursive_call; } if (stack.Count == 0) return state.PdbScope; // Return from recursive call, and execute the last part of the for loop var newPdbScope = state.PdbScope; state = stack.Pop(); state.PdbScope.Scopes.Add(newPdbScope); state.ChildrenIndex++; goto do_return; } static Instruction GetInstruction(IList instrs, int offset, ref int index) { if (instrs.Count > 0 && offset > instrs[instrs.Count - 1].Offset) return null; for (int i = index; i < instrs.Count; i++) { var instr = instrs[i]; if (instr.Offset < offset) continue; if (instr.Offset == offset) { index = i; return instr; } break; } for (int i = 0; i < index; i++) { var instr = instrs[i]; if (instr.Offset < offset) continue; if (instr.Offset == offset) { index = i; return instr; } break; } return null; } internal void InitializeCustomDebugInfos(MDToken token, GenericParamContext gpContext, IList result) { Debug.Assert(token.Table != Table.Method, "Methods get initialized when reading the method bodies"); reader?.GetCustomDebugInfos(token.ToInt32(), gpContext, result); } internal void Dispose() => reader?.Dispose(); } enum Compiler { Other, VisualBasic, } }