/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.ObjectModel; using System.ComponentModel.Composition; using System.Diagnostics; using System.Linq; using dnlib.DotNet; using dnSpy.Contracts.Debugger.DotNet.Metadata; using dnSpy.Contracts.Decompiler; using dnSpy.Contracts.Documents; using dnSpy.Contracts.Documents.Tabs; using dnSpy.Contracts.Documents.Tabs.DocViewer; using dnSpy.Contracts.Metadata; using dnSpy.Debugger.DotNet.Metadata; using dnSpy.Debugger.DotNet.UI; namespace dnSpy.Debugger.DotNet.Code { abstract class DotNetReferenceNavigator : ReferenceNavigator { public const uint EPILOG = 0xFFFFFFFF; public const uint PROLOG = 0xFFFFFFFE; public abstract void GoToLocation(IDocumentTab tab, MethodDef method, ModuleTokenId module, uint offset, bool newTab); } [ExportReferenceNavigator] [Export(typeof(DotNetReferenceNavigator))] sealed class DotNetReferenceNavigatorImpl : DotNetReferenceNavigator { readonly UIDispatcher uiDispatcher; readonly IDocumentTabService documentTabService; readonly Lazy dbgMetadataService; [ImportingConstructor] DotNetReferenceNavigatorImpl(UIDispatcher uiDispatcher, IDocumentTabService documentTabService, Lazy dbgMetadataService) { this.uiDispatcher = uiDispatcher; this.documentTabService = documentTabService; this.dbgMetadataService = dbgMetadataService; } public override bool GoTo(object reference, ReadOnlyCollection options) { switch (reference) { case DotNetMethodBodyReference bodyRef: GoTo(bodyRef, options); // Always return true even if GoTo() fails (eg. module not found). Otherwise an empty // page will be shown when the default handler tries to show the reference. return true; case DotNetTokenReference tokenRef: GoTo(tokenRef, options); // Always return true, see comment above return true; } return false; } ModuleDef? GetModule(ModuleId module, ReadOnlyCollection options) => dbgMetadataService.Value.TryGetMetadata(module, DbgLoadModuleOptions.None); bool GoTo(DotNetMethodBodyReference bodyRef, ReadOnlyCollection options) { bool newTab = options.Any(a => StringComparer.Ordinal.Equals(PredefinedReferenceNavigatorOptions.NewTab, a)); var module = GetModule(bodyRef.Module, options); if (module is null) return false; var method = module.ResolveToken(bodyRef.Token) as MethodDef; if (method is null) return false; uint offset = bodyRef.Offset; if (offset == DotNetMethodBodyReference.PROLOG) offset = PROLOG; else if (offset == DotNetMethodBodyReference.EPILOG) offset = EPILOG; var tab = documentTabService.GetOrCreateActiveTab(); GoToLocation(tab, method, new ModuleTokenId(bodyRef.Module, bodyRef.Token), offset, newTab); return true; } bool GoTo(DotNetTokenReference tokenRef, ReadOnlyCollection options) { bool newTab = options.Any(a => StringComparer.Ordinal.Equals(PredefinedReferenceNavigatorOptions.NewTab, a)); var module = GetModule(tokenRef.Module, options); if (module is null) return false; var def = module.ResolveToken(tokenRef.Token) as IMemberDef; if (def is null) return false; bool found = documentTabService.DocumentTreeView.FindNode(def.Module) is not null; if (found) { documentTabService.FollowReference(def, newTab, true, e => { Debug.Assert(e.Tab.UIContext is IDocumentViewer); if (e.Success && !e.HasMovedCaret) { MoveCaretTo(e.Tab.UIContext as IDocumentViewer, def); e.HasMovedCaret = true; } }); return true; } // If it wasn't found, it will be added to the treeview with a slight delay, make sure our code gets executed // after it's happened. uiDispatcher.UIBackground(() => { documentTabService.FollowReference(def, newTab, true, e => { Debug.Assert(e.Tab.UIContext is IDocumentViewer); if (e.Success && !e.HasMovedCaret) { MoveCaretTo(e.Tab.UIContext as IDocumentViewer, def); e.HasMovedCaret = true; } }); }); return true; } static bool MoveCaretTo(IDocumentViewer? documentViewer, IMemberDef def) { if (documentViewer is null) return false; var data = documentViewer.ReferenceCollection.FirstOrNull(a => a.Data.IsDefinition && a.Data.Reference == def); if (data is null) return false; documentViewer.MoveCaretToPosition(data.Value.Span.Start); return true; } public override void GoToLocation(IDocumentTab tab, MethodDef method, ModuleTokenId module, uint offset, bool newTab) { bool specialIpOffset; if (offset == EPILOG) { specialIpOffset = true; var mod = dbgMetadataService.Value.TryGetMetadata(module.Module, DbgLoadModuleOptions.AutoLoaded); if (mod?.ResolveToken(module.Token) is MethodDef md && md.Body is not null && md.Body.Instructions.Count > 0) offset = md.Body.Instructions[md.Body.Instructions.Count - 1].Offset; else return; } else if (offset == PROLOG) { specialIpOffset = true; offset = 0; } else specialIpOffset = false; GoToLocationCore(tab, method, module, offset, specialIpOffset, newTab, canRefreshMethods: true); } void GoToLocationCore(IDocumentTab tab, MethodDef method, ModuleTokenId module, uint offset, bool specialIpOffset, bool newTab, bool canRefreshMethods) { uiDispatcher.VerifyAccess(); if (tab is null || method is null) return; // The file could've been added lazily to the list so add a short delay before we select it uiDispatcher.UIBackground(() => { tab.FollowReference(method, newTab, e => { Debug.Assert(e.Tab.UIContext is IDocumentViewer); if (e.Success && !e.HasMovedCaret) { MoveCaretToCurrentStatement(e.Tab.UIContext as IDocumentViewer, method, module, offset, specialIpOffset, canRefreshMethods, newTab: false); e.HasMovedCaret = true; } }); }); } bool MoveCaretToCurrentStatement(IDocumentViewer? documentViewer, MethodDef method, ModuleTokenId module, uint offset, bool specialIpOffset, bool canRefreshMethods, bool newTab) { if (documentViewer is null) return false; if (MoveCaretTo(documentViewer, module, offset)) return true; if (!canRefreshMethods) return false; RefreshMethodBodies(documentViewer, method, module, offset, specialIpOffset, newTab: false); return false; } static bool MoveCaretTo(IDocumentViewer documentViewer, ModuleTokenId module, uint offset) { if (documentViewer is null) return false; if (!VerifyAndGetCurrentDebuggedMethod(documentViewer, module, out var methodDebugService)) return false; var sourceStatement = methodDebugService.TryGetMethodDebugInfo(module)!.GetSourceStatementByCodeOffset(offset); if (sourceStatement is null) return false; documentViewer.MoveCaretToPosition(sourceStatement.Value.TextSpan.Start); return true; } static bool VerifyAndGetCurrentDebuggedMethod(IDocumentViewer documentViewer, ModuleTokenId token, out IMethodDebugService methodDebugService) { methodDebugService = documentViewer.GetMethodDebugService(); return methodDebugService.TryGetMethodDebugInfo(token) is not null; } void RefreshMethodBodies(IDocumentViewer documentViewer, MethodDef method, ModuleTokenId module, uint offset, bool specialIpOffset, bool newTab) { // If it's in the prolog/epilog, ignore it if (specialIpOffset) return; if (module.Module.IsDynamic) return; var body = method.Body; if (body is null) return; // If the offset is a valid instruction in the body, the method is probably not encrypted if (body.Instructions.Any(i => i.Offset == offset)) return; var modNode = documentTabService.DocumentTreeView.FindNode(method.Module); if (modNode is null) return; if (modNode.Document is MemoryModuleDefDocument memFile) memFile.UpdateMemory(); else { var mod = dbgMetadataService.Value.TryGetMetadata(module.Module, DbgLoadModuleOptions.ForceMemory | DbgLoadModuleOptions.AutoLoaded); var md = mod?.ResolveToken(module.Token) as MethodDef; if (md is null) return; method = md; } GoToLocationCore(documentViewer.DocumentTab!, method, module, offset, specialIpOffset, newTab, canRefreshMethods: false); } } }