/*
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.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Threading;
using dnlib.DotNet;
using dnSpy.Contracts.Debugger;
using dnSpy.Contracts.Debugger.DotNet.Code;
using dnSpy.Contracts.Debugger.DotNet.Metadata;
using dnSpy.Contracts.Decompiler;
using dnSpy.Contracts.Metadata;
using dnSpy.Decompiler.Utils;
namespace dnSpy.Debugger.DotNet.Code {
readonly struct MethodDebugInfoResult {
public int MethodVersion { get; }
public DbgMethodDebugInfo? DebugInfo { get; }
public DbgMethodDebugInfo? StateMachineDebugInfo { get; }
public MethodDebugInfoResult(int methodVersion, DbgMethodDebugInfo? debugInfo, DbgMethodDebugInfo? stateMachineDebugInfo) {
if (methodVersion < 1)
throw new ArgumentOutOfRangeException(nameof(methodVersion));
MethodVersion = methodVersion;
DebugInfo = debugInfo;
StateMachineDebugInfo = stateMachineDebugInfo;
}
}
abstract class DbgMethodDebugInfoProvider {
public abstract MethodDebugInfoResult GetMethodDebugInfo(IDecompiler decompiler, DbgModule module, uint token, CancellationToken cancellationToken);
public abstract MethodDebugInfoResult GetMethodDebugInfo(DbgRuntime runtime, IDecompiler decompiler, IDbgDotNetCodeLocation location, CancellationToken cancellationToken);
}
[Export(typeof(DbgMethodDebugInfoProvider))]
sealed class DbgMethodDebugInfoProviderImpl : DbgMethodDebugInfoProvider {
const DbgLoadModuleOptions loadModuleOptions = DbgLoadModuleOptions.AutoLoaded;
const int MAX_CACHED_DEBUG_INFOS = 5;
readonly DbgMetadataService dbgMetadataService;
[ImportingConstructor]
DbgMethodDebugInfoProviderImpl(DbgMetadataService dbgMetadataService) => this.dbgMetadataService = dbgMetadataService;
public override MethodDebugInfoResult GetMethodDebugInfo(IDecompiler decompiler, DbgModule module, uint token, CancellationToken cancellationToken) {
var mdModule = dbgMetadataService.TryGetMetadata(module, loadModuleOptions);
var key = new MethodDebugInfoResultKey(module, token);
return GetMethodDebugInfo(module.Runtime, key, decompiler, mdModule, token, cancellationToken);
}
public override MethodDebugInfoResult GetMethodDebugInfo(DbgRuntime runtime, IDecompiler decompiler, IDbgDotNetCodeLocation location, CancellationToken cancellationToken) {
ModuleDef? mdModule;
MethodDebugInfoResultKey key;
if (location.DbgModule is DbgModule dbgModule) {
key = new MethodDebugInfoResultKey(dbgModule, location.Token);
mdModule = dbgMetadataService.TryGetMetadata(dbgModule, loadModuleOptions);
}
else {
key = new MethodDebugInfoResultKey(location.Module, location.Token);
mdModule = dbgMetadataService.TryGetMetadata(location.Module, loadModuleOptions);
}
return GetMethodDebugInfo(runtime, key, decompiler, mdModule, location.Token, cancellationToken);
}
readonly struct MethodDebugInfoResultKey {
readonly uint token;
readonly DbgModule? module;
readonly ModuleId moduleId;
readonly int refreshedVersion;
public MethodDebugInfoResultKey(DbgModule module, uint token) {
this.token = token;
moduleId = default;
this.module = module;
refreshedVersion = module.RefreshedVersion;
}
public MethodDebugInfoResultKey(ModuleId moduleId, uint token) {
this.token = token;
this.moduleId = moduleId;
module = null;
refreshedVersion = 0;
}
public bool Equals(MethodDebugInfoResultKey other) =>
token == other.token &&
module == other.module &&
refreshedVersion == other.refreshedVersion &&
moduleId == other.moduleId;
}
sealed class RuntimeState {
public readonly object LockObj = new object();
public readonly List<(MethodDebugInfoResultKey key, MethodDebugInfoResult result)> DebugInfos = new List<(MethodDebugInfoResultKey key, MethodDebugInfoResult result)>(MAX_CACHED_DEBUG_INFOS);
}
MethodDebugInfoResult GetMethodDebugInfo(DbgRuntime runtime, in MethodDebugInfoResultKey key, IDecompiler decompiler, ModuleDef? mdModule, uint token, CancellationToken cancellationToken) {
Debug2.Assert(mdModule is not null);
if (mdModule is null)
return default;
var state = runtime.GetOrCreateData();
var debugInfos = state.DebugInfos;
lock (state.LockObj) {
for (int i = debugInfos.Count - 1; i >= 0; i--) {
var info = debugInfos[i];
if (info.key.Equals(key)) {
if ((info.result.DebugInfo is not null && info.result.DebugInfo.DebugInfoVersion != decompiler.Settings.Version) ||
(info.result.StateMachineDebugInfo is not null && info.result.StateMachineDebugInfo.DebugInfoVersion != decompiler.Settings.Version)) {
debugInfos.RemoveAt(i);
continue;
}
if (i != debugInfos.Count - 1) {
debugInfos.RemoveAt(i);
debugInfos.Add(info);
}
return info.result;
}
}
}
var result = GetMethodDebugInfoNonCached(decompiler, mdModule, token, cancellationToken);
if (result.DebugInfo is null)
return default;
lock (state.LockObj) {
if (debugInfos.Count == MAX_CACHED_DEBUG_INFOS)
debugInfos.RemoveAt(0);
debugInfos.Add((key, result));
}
return result;
}
MethodDebugInfoResult GetMethodDebugInfoNonCached(IDecompiler decompiler, ModuleDef mdModule, uint token, CancellationToken cancellationToken) {
cancellationToken.ThrowIfCancellationRequested();
var method = mdModule.ResolveToken(token) as MethodDef;
// Could be null if it's a dynamic assembly. It will get refreshed later and we'll get called again.
if (method is null)
return default;
if (!StateMachineHelpers.TryGetKickoffMethod(method, out var containingMethod))
containingMethod = method;
var decContext = new DecompilationContext {
CancellationToken = cancellationToken,
CalculateILSpans = true,
// This is only needed when decompiling more than one body
AsyncMethodBodyDecompilation = false,
};
var info = TryDecompileAndGetDebugInfo(decompiler, containingMethod, token, decContext, cancellationToken);
if (info.debugInfo is null && containingMethod != method) {
// The decompiler can't decompile the iterator / async method, try again,
// but only decompile the MoveNext method
info = TryDecompileAndGetDebugInfo(decompiler, method, token, decContext, cancellationToken);
}
if (info.debugInfo is null && method.Body is null) {
var scope = new DbgMethodDebugScope(new DbgILSpan(0, 0), Array.Empty(), Array.Empty(), Array.Empty());
info = (new DbgMethodDebugInfo(DbgCompilerKind.Unknown, -1, method, null, Array.Empty(), scope, null), null);
}
if (info.debugInfo is null)
return default;
// We don't support EnC so the version is always 1
const int methodVersion = 1;
return new MethodDebugInfoResult(methodVersion, info.debugInfo, info.stateMachineDebugInfo);
}
static class DecompilerOutputImplCache {
static DecompilerOutputImpl? instance;
public static DecompilerOutputImpl Alloc() => Interlocked.Exchange(ref instance, null) ?? new DecompilerOutputImpl();
public static void Free(ref DecompilerOutputImpl? inst) {
var tmp = inst!;
inst = null;
tmp.Clear();
instance = tmp;
}
}
(DbgMethodDebugInfo debugInfo, DbgMethodDebugInfo? stateMachineDebugInfo) TryDecompileAndGetDebugInfo(IDecompiler decompiler, MethodDef method, uint methodToken, DecompilationContext decContext, CancellationToken cancellationToken) {
DecompilerOutputImpl? output = DecompilerOutputImplCache.Alloc();
output.Initialize(methodToken);
decompiler.Decompile(method, output, decContext);
var info = output.TryGetMethodDebugInfo();
DecompilerOutputImplCache.Free(ref output);
cancellationToken.ThrowIfCancellationRequested();
return info;
}
}
}