mirror of
https://github.com/AssetRipper/ILSpy.git
synced 2026-11-13 20:05:48 -10:00
1541 lines
57 KiB
C#
1541 lines
57 KiB
C#
// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
|
|
//
|
|
// 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.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection.Metadata;
|
|
|
|
using ICSharpCode.Decompiler.CSharp;
|
|
using ICSharpCode.Decompiler.IL.Transforms;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
using ICSharpCode.Decompiler.Util;
|
|
|
|
namespace ICSharpCode.Decompiler.IL.ControlFlow
|
|
{
|
|
public class YieldReturnDecompiler : IILTransform
|
|
{
|
|
// For a description on the code generated by the C# compiler for yield return:
|
|
// http://csharpindepth.com/Articles/Chapter6/IteratorBlockImplementation.aspx
|
|
|
|
// The idea here is:
|
|
// - Figure out whether the current method is instanciating an enumerator
|
|
// - Figure out which of the fields is the state field
|
|
// - Construct an exception table based on states. This allows us to determine, for each state, what the parent try block is.
|
|
|
|
// See http://community.sharpdevelop.net/blogs/danielgrunwald/archive/2011/03/06/ilspy-yield-return.aspx
|
|
// for a description of this step.
|
|
|
|
ILTransformContext context;
|
|
MetadataReader metadata;
|
|
|
|
/// <summary>The type that contains the function being decompiled.</summary>
|
|
TypeDefinitionHandle currentType;
|
|
|
|
/// <summary>The compiler-generated enumerator class.</summary>
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks>
|
|
TypeDefinitionHandle enumeratorType;
|
|
|
|
/// <summary>The constructor of the compiler-generated enumerator class.</summary>
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks>
|
|
MethodDefinitionHandle enumeratorCtor;
|
|
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks>
|
|
bool isCompiledWithMono;
|
|
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern() or ConstructExceptionTable()</remarks>
|
|
bool isCompiledWithVisualBasic;
|
|
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern()</remarks>
|
|
/// <remarks>If this is true, then isCompiledWithVisualBasic is also true.</remarks>
|
|
bool isCompiledWithLegacyVisualBasic;
|
|
|
|
/// <summary>The dispose method of the compiler-generated enumerator class.</summary>
|
|
/// <remarks>Set in ConstructExceptionTable()</remarks>
|
|
MethodDefinitionHandle disposeMethod;
|
|
|
|
/// <summary>The field in the compiler-generated class holding the current state of the state machine</summary>
|
|
/// <remarks>Set in AnalyzeCtor() for MS, MatchEnumeratorCreationPattern() or AnalyzeMoveNext() for Mono</remarks>
|
|
IField stateField;
|
|
|
|
/// <summary>The backing field of the 'Current' property in the compiler-generated class</summary>
|
|
/// <remarks>Set in AnalyzeCurrentProperty()</remarks>
|
|
IField currentField;
|
|
|
|
/// <summary>The disposing field of the compiler-generated enumerator class.</summary>
|
|
/// <remarks>Set in ConstructExceptionTable() for assembly compiled with Mono</remarks>
|
|
IField disposingField;
|
|
|
|
/// <summary>Maps the fields of the compiler-generated class to the original parameters.</summary>
|
|
/// <remarks>Set in MatchEnumeratorCreationPattern() and ResolveIEnumerableIEnumeratorFieldMapping()</remarks>
|
|
readonly Dictionary<IField, ILVariable> fieldToParameterMap = new Dictionary<IField, ILVariable>();
|
|
|
|
/// <summary>This dictionary stores the information extracted from the Dispose() method:
|
|
/// for each "Finally Method", it stores the set of states for which the method is being called.</summary>
|
|
/// <remarks>Set in ConstructExceptionTable()</remarks>
|
|
Dictionary<IMethod, LongSet> finallyMethodToStateRange;
|
|
|
|
/// <summary>
|
|
/// For each finally method, stores the target state when entering the finally block,
|
|
/// and the decompiled code of the finally method body.
|
|
/// </summary>
|
|
readonly Dictionary<IMethod, (int? outerState, ILFunction function)> decompiledFinallyMethods = new Dictionary<IMethod, (int? outerState, ILFunction body)>();
|
|
|
|
/// <summary>
|
|
/// Temporary stores for 'yield break'.
|
|
/// </summary>
|
|
readonly List<StLoc> returnStores = new List<StLoc>();
|
|
|
|
/// <summary>
|
|
/// Local bool variable in MoveNext() that signifies whether to skip finally bodies.
|
|
/// </summary>
|
|
ILVariable skipFinallyBodies;
|
|
|
|
/// <summary>
|
|
/// Local bool variable in MoveNext() that signifies whether to execute finally bodies.
|
|
/// </summary>
|
|
ILVariable doFinallyBodies;
|
|
|
|
/// <summary>
|
|
/// Set of variables might hold copies of the generated state field.
|
|
/// </summary>
|
|
HashSet<ILVariable> cachedStateVars;
|
|
|
|
#region Run() method
|
|
public void Run(ILFunction function, ILTransformContext context)
|
|
{
|
|
if (!context.Settings.YieldReturn)
|
|
return; // abort if enumerator decompilation is disabled
|
|
this.context = context;
|
|
this.metadata = context.PEFile.Metadata;
|
|
this.currentType = metadata.GetMethodDefinition((MethodDefinitionHandle)context.Function.Method.MetadataToken).GetDeclaringType();
|
|
this.enumeratorType = default;
|
|
this.enumeratorCtor = default;
|
|
this.isCompiledWithMono = false;
|
|
this.isCompiledWithVisualBasic = false;
|
|
this.isCompiledWithLegacyVisualBasic = false;
|
|
this.stateField = null;
|
|
this.currentField = null;
|
|
this.disposingField = null;
|
|
this.fieldToParameterMap.Clear();
|
|
this.finallyMethodToStateRange = null;
|
|
this.decompiledFinallyMethods.Clear();
|
|
this.returnStores.Clear();
|
|
this.skipFinallyBodies = null;
|
|
this.doFinallyBodies = null;
|
|
this.cachedStateVars = null;
|
|
if (!MatchEnumeratorCreationPattern(function))
|
|
return;
|
|
BlockContainer newBody;
|
|
try
|
|
{
|
|
AnalyzeCtor();
|
|
AnalyzeCurrentProperty();
|
|
ResolveIEnumerableIEnumeratorFieldMapping();
|
|
ConstructExceptionTable();
|
|
newBody = AnalyzeMoveNext(function);
|
|
}
|
|
catch (SymbolicAnalysisFailedException ex)
|
|
{
|
|
function.Warnings.Add($"yield-return decompiler failed: {ex.Message}");
|
|
return;
|
|
}
|
|
|
|
context.Step("Replacing body with MoveNext() body", function);
|
|
function.IsIterator = true;
|
|
function.StateMachineCompiledWithMono = isCompiledWithMono;
|
|
function.StateMachineCompiledWithLegacyVisualBasic = isCompiledWithLegacyVisualBasic;
|
|
var oldBody = function.Body;
|
|
function.Body = newBody;
|
|
// register any locals used in newBody
|
|
function.Variables.AddRange(newBody.Descendants.OfType<IInstructionWithVariableOperand>().Select(inst => inst.Variable).Distinct());
|
|
|
|
PrintFinallyMethodStateRanges(newBody);
|
|
|
|
// Add state machine field meta-data to parameter ILVariables.
|
|
foreach (var (f, p) in fieldToParameterMap)
|
|
{
|
|
p.StateMachineField = f;
|
|
}
|
|
|
|
context.Step("Delete unreachable blocks", function);
|
|
|
|
if (isCompiledWithMono || isCompiledWithVisualBasic)
|
|
{
|
|
// mono has try-finally inline (like async on MS); we also need to sort nested blocks:
|
|
foreach (var nestedContainer in newBody.Blocks.SelectMany(c => c.Descendants).OfType<BlockContainer>())
|
|
{
|
|
nestedContainer.SortBlocks(deleteUnreachableBlocks: true);
|
|
}
|
|
// We need to clean up nested blocks before the main block, so that edges from unreachable code
|
|
// in nested containers into the main container are removed before we clean up the main container.
|
|
}
|
|
// Note: because this only deletes blocks outright, the 'stateChanges' entries remain valid
|
|
// (though some may point to now-deleted blocks)
|
|
newBody.SortBlocks(deleteUnreachableBlocks: true);
|
|
function.CheckInvariant(ILPhase.Normal);
|
|
|
|
try
|
|
{
|
|
if (isCompiledWithMono)
|
|
{
|
|
CleanSkipFinallyBodies(function);
|
|
}
|
|
else if (isCompiledWithLegacyVisualBasic)
|
|
{
|
|
CleanDoFinallyBodies(function);
|
|
}
|
|
else if (isCompiledWithVisualBasic)
|
|
{
|
|
CleanFinallyStateChecks(function);
|
|
}
|
|
else
|
|
{
|
|
DecompileFinallyBlocks();
|
|
ReconstructTryFinallyBlocks(function);
|
|
}
|
|
}
|
|
catch (SymbolicAnalysisFailedException ex)
|
|
{
|
|
// Revert the yield-return transformation
|
|
context.Step("Transform failed, roll it back", function);
|
|
function.IsIterator = false;
|
|
function.StateMachineCompiledWithMono = false;
|
|
function.StateMachineCompiledWithLegacyVisualBasic = false;
|
|
function.Body = oldBody;
|
|
function.Variables.RemoveDead();
|
|
function.Warnings.Add($"yield-return decompiler failed: {ex.Message}");
|
|
return;
|
|
}
|
|
|
|
context.Step("Translate fields to local accesses", function);
|
|
TranslateFieldsToLocalAccess(function, function, fieldToParameterMap, isCompiledWithMono);
|
|
|
|
// On mono, we still need to remove traces of the state variable(s):
|
|
if (isCompiledWithMono || isCompiledWithVisualBasic)
|
|
{
|
|
if (fieldToParameterMap.TryGetValue(stateField, out var stateVar))
|
|
{
|
|
returnStores.AddRange(stateVar.StoreInstructions.OfType<StLoc>());
|
|
}
|
|
foreach (var cachedStateVar in cachedStateVars)
|
|
{
|
|
returnStores.AddRange(cachedStateVar.StoreInstructions.OfType<StLoc>());
|
|
}
|
|
}
|
|
|
|
if (returnStores.Count > 0)
|
|
{
|
|
context.Step("Remove temporaries", function);
|
|
foreach (var store in returnStores)
|
|
{
|
|
if (store.Variable.LoadCount == 0 && store.Variable.AddressCount == 0 && store.Parent is Block block)
|
|
{
|
|
Debug.Assert(SemanticHelper.IsPure(store.Value.Flags));
|
|
block.Instructions.Remove(store);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Re-run control flow simplification over the newly constructed set of gotos,
|
|
// and inlining because TranslateFieldsToLocalAccess() might have opened up new inlining opportunities.
|
|
function.RunTransforms(CSharpDecompiler.EarlyILTransforms(), context);
|
|
}
|
|
#endregion
|
|
|
|
#region Match the enumerator creation pattern
|
|
bool MatchEnumeratorCreationPattern(ILFunction function)
|
|
{
|
|
Block body = SingleBlock(function.Body);
|
|
if (body == null || body.Instructions.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ILInstruction newObj;
|
|
if (body.Instructions.Count == 1)
|
|
{
|
|
// No parameters passed to enumerator (not even 'this'):
|
|
// ret(newobj(...))
|
|
if (!body.Instructions[0].MatchReturn(out newObj))
|
|
return false;
|
|
if (MatchEnumeratorCreationNewObj(newObj))
|
|
{
|
|
return true;
|
|
}
|
|
else if (MatchMonoEnumeratorCreationNewObj(newObj))
|
|
{
|
|
isCompiledWithMono = true;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If there's parameters passed to the helper class, the class instance is first
|
|
// stored in a variable, then the parameters are copied over, then the instance is returned.
|
|
|
|
int pos = 0;
|
|
|
|
// stloc(var_1, newobj(..))
|
|
if (!body.Instructions[pos].MatchStLoc(out var var1, out newObj))
|
|
return false;
|
|
if (MatchEnumeratorCreationNewObj(newObj))
|
|
{
|
|
pos++; // OK
|
|
}
|
|
else if (MatchMonoEnumeratorCreationNewObj(newObj))
|
|
{
|
|
pos++;
|
|
if (TransformDisplayClassUsage.ValidateConstructor(context, ((NewObj)newObj).Method))
|
|
{
|
|
isCompiledWithMono = true;
|
|
}
|
|
else
|
|
{
|
|
isCompiledWithVisualBasic = true;
|
|
isCompiledWithLegacyVisualBasic = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool stateFieldInitialized = false;
|
|
for (; pos < body.Instructions.Count; pos++)
|
|
{
|
|
// stfld(..., ldloc(var_1), ldloc(parameter))
|
|
// or (in structs): stfld(..., ldloc(var_1), ldobj(ldloc(this)))
|
|
if (!body.Instructions[pos].MatchStFld(out var ldloc, out var storedField, out var value))
|
|
break;
|
|
if (!ldloc.MatchLdLoc(var1))
|
|
{
|
|
return false;
|
|
}
|
|
if (value.MatchLdLoc(out var parameter) && parameter.Kind == VariableKind.Parameter)
|
|
{
|
|
fieldToParameterMap[(IField)storedField.MemberDefinition] = parameter;
|
|
}
|
|
else if (value is LdObj ldobj && ldobj.Target.MatchLdThis())
|
|
{
|
|
// copy of 'this' in struct
|
|
fieldToParameterMap[(IField)storedField.MemberDefinition] = ((LdLoc)ldobj.Target).Variable;
|
|
}
|
|
else if ((isCompiledWithMono || isCompiledWithLegacyVisualBasic) && (value.MatchLdcI4(-2) || value.MatchLdcI4(-1) || value.MatchLdcI4(0)))
|
|
{
|
|
stateField = (IField)storedField.MemberDefinition;
|
|
stateFieldInitialized = true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// In debug builds, the compiler may copy the var1 into another variable (var2) before returning it.
|
|
if (body.Instructions[pos].MatchStLoc(out var var2, out var ldlocForStloc2)
|
|
&& ldlocForStloc2.MatchLdLoc(var1))
|
|
{
|
|
// stloc(var_2, ldloc(var_1))
|
|
pos++;
|
|
}
|
|
if (isCompiledWithMono && !stateFieldInitialized)
|
|
{
|
|
// Mono initializes the state field separately:
|
|
// (but not if it's left at the default value 0)
|
|
if (body.Instructions[pos].MatchStFld(out var target, out var field, out var value)
|
|
&& target.MatchLdLoc(var2 ?? var1)
|
|
&& (value.MatchLdcI4(-2) || value.MatchLdcI4(0)))
|
|
{
|
|
stateField = (IField)field.MemberDefinition;
|
|
pos++;
|
|
}
|
|
}
|
|
if (body.Instructions[pos].MatchReturn(out var retVal)
|
|
&& retVal.MatchLdLoc(var2 ?? var1))
|
|
{
|
|
// ret(ldloc(var_2))
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Matches the body of a method as a single basic block.
|
|
/// </summary>
|
|
internal static Block SingleBlock(ILInstruction body)
|
|
{
|
|
var block = body as Block;
|
|
if (body is BlockContainer blockContainer && blockContainer.Blocks.Count == 1)
|
|
{
|
|
block = blockContainer.Blocks.Single();
|
|
}
|
|
return block;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Matches the newobj instruction that creates an instance of the compiler-generated enumerator helper class.
|
|
/// </summary>
|
|
bool MatchEnumeratorCreationNewObj(ILInstruction inst)
|
|
{
|
|
return MatchEnumeratorCreationNewObj(inst, metadata, currentType,
|
|
out enumeratorCtor, out enumeratorType);
|
|
}
|
|
|
|
internal static bool MatchEnumeratorCreationNewObj(ILInstruction inst,
|
|
MetadataReader metadata, TypeDefinitionHandle currentType,
|
|
out MethodDefinitionHandle enumeratorCtor, out TypeDefinitionHandle enumeratorType)
|
|
{
|
|
enumeratorCtor = default;
|
|
enumeratorType = default;
|
|
// newobj(CurrentType/...::.ctor, ldc.i4(-2))
|
|
if (!(inst is NewObj newObj))
|
|
return false;
|
|
if (newObj.Arguments.Count != 1)
|
|
return false;
|
|
if (!newObj.Arguments[0].MatchLdcI4(out int initialState))
|
|
return false;
|
|
if (!(initialState == -2 || initialState == 0))
|
|
return false;
|
|
var handle = newObj.Method.MetadataToken;
|
|
enumeratorCtor = handle.IsNil || handle.Kind != HandleKind.MethodDefinition ? default : (MethodDefinitionHandle)handle;
|
|
enumeratorType = enumeratorCtor.IsNil ? default : metadata.GetMethodDefinition(enumeratorCtor).GetDeclaringType();
|
|
return (enumeratorType.IsNil ? default : metadata.GetTypeDefinition(enumeratorType).GetDeclaringType()) == currentType
|
|
&& IsCompilerGeneratorEnumerator(enumeratorType, metadata);
|
|
}
|
|
|
|
bool MatchMonoEnumeratorCreationNewObj(ILInstruction inst)
|
|
{
|
|
// mcs generates iterators that take no parameters in the ctor
|
|
if (!(inst is NewObj newObj))
|
|
return false;
|
|
if (newObj.Arguments.Count != 0)
|
|
return false;
|
|
var handle = newObj.Method.MetadataToken;
|
|
enumeratorCtor = handle.IsNil || handle.Kind != HandleKind.MethodDefinition ? default : (MethodDefinitionHandle)handle;
|
|
enumeratorType = enumeratorCtor.IsNil ? default : metadata.GetMethodDefinition(enumeratorCtor).GetDeclaringType();
|
|
return (enumeratorType.IsNil ? default : metadata.GetTypeDefinition(enumeratorType).GetDeclaringType()) == currentType
|
|
&& IsCompilerGeneratorEnumerator(enumeratorType, metadata);
|
|
}
|
|
|
|
public static bool IsCompilerGeneratorEnumerator(TypeDefinitionHandle type, MetadataReader metadata)
|
|
{
|
|
TypeDefinition td;
|
|
if (type.IsNil || !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil)
|
|
return false;
|
|
foreach (var i in td.GetInterfaceImplementations())
|
|
{
|
|
var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata);
|
|
if (!tr.IsNested && tr.TopLevelTypeName.Namespace == "System.Collections" && tr.TopLevelTypeName.Name == "IEnumerator")
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
#endregion
|
|
|
|
#region Figure out what the 'state' field is (analysis of .ctor())
|
|
/// <summary>
|
|
/// Looks at the enumerator's ctor and figures out which of the fields holds the state.
|
|
/// </summary>
|
|
void AnalyzeCtor()
|
|
{
|
|
Block body = SingleBlock(CreateILAst(enumeratorCtor, context).Body);
|
|
if (body == null)
|
|
throw new SymbolicAnalysisFailedException("Missing enumeratorCtor.Body");
|
|
foreach (var inst in body.Instructions)
|
|
{
|
|
if (inst.MatchStFld(out var target, out var field, out var value)
|
|
&& target.MatchLdThis()
|
|
&& value.MatchLdLoc(out var arg)
|
|
&& arg.Kind == VariableKind.Parameter && arg.Index == 0)
|
|
{
|
|
stateField = (IField)field.MemberDefinition;
|
|
}
|
|
}
|
|
if (stateField == null && !isCompiledWithMono)
|
|
throw new SymbolicAnalysisFailedException("Could not find stateField");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates ILAst for the specified method, optimized up to before the 'YieldReturn' step.
|
|
/// </summary>
|
|
internal static ILFunction CreateILAst(MethodDefinitionHandle method, ILTransformContext context)
|
|
{
|
|
var metadata = context.PEFile.Metadata;
|
|
if (method.IsNil)
|
|
throw new SymbolicAnalysisFailedException("Method not found");
|
|
|
|
var methodDef = metadata.GetMethodDefinition(method);
|
|
if (!methodDef.HasBody())
|
|
throw new SymbolicAnalysisFailedException($"Method {methodDef.Name} has no body");
|
|
|
|
GenericContext genericContext = context.Function.GenericContext;
|
|
genericContext = new GenericContext(
|
|
classTypeParameters: (genericContext.ClassTypeParameters ?? EmptyList<ITypeParameter>.Instance)
|
|
.Concat(genericContext.MethodTypeParameters ?? EmptyList<ITypeParameter>.Instance).ToArray(),
|
|
methodTypeParameters: null);
|
|
var body = context.TypeSystem.MainModule.MetadataFile.GetMethodBody(methodDef.RelativeVirtualAddress);
|
|
var il = context.CreateILReader()
|
|
.ReadIL(method, body, genericContext, ILFunctionKind.TopLevelFunction, context.CancellationToken);
|
|
il.RunTransforms(CSharpDecompiler.EarlyILTransforms(true),
|
|
new ILTransformContext(il, context.TypeSystem, context.DebugInfo, context.Settings) {
|
|
CancellationToken = context.CancellationToken,
|
|
DecompileRun = context.DecompileRun
|
|
});
|
|
return il;
|
|
}
|
|
#endregion
|
|
|
|
#region Figure out what the 'current' field is (analysis of get_Current())
|
|
/// <summary>
|
|
/// Looks at the enumerator's get_Current method and figures out which of the fields holds the current value.
|
|
/// </summary>
|
|
void AnalyzeCurrentProperty()
|
|
{
|
|
MethodDefinitionHandle getCurrentMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(
|
|
m => IsMethod(m, "get_Current"));
|
|
Block body = SingleBlock(CreateILAst(getCurrentMethod, context).Body);
|
|
if (body == null)
|
|
throw new SymbolicAnalysisFailedException("get_Current has no body");
|
|
if (body.Instructions.Count == 1)
|
|
{
|
|
// release builds directly return the current field
|
|
// ret(ldfld F(ldloc(this)))
|
|
if (body.Instructions[0].MatchReturn(out var retVal)
|
|
&& retVal.MatchLdFld(out var target, out var field)
|
|
&& target.MatchLdThis())
|
|
{
|
|
currentField = (IField)field.MemberDefinition;
|
|
}
|
|
}
|
|
else if (body.Instructions.Count == 2)
|
|
{
|
|
// debug builds store the return value in a temporary
|
|
// stloc V = ldfld F(ldloc(this))
|
|
// ret(ldloc V)
|
|
if (body.Instructions[0].MatchStLoc(out var v, out var ldfld)
|
|
&& ldfld.MatchLdFld(out var target, out var field)
|
|
&& target.MatchLdThis()
|
|
&& body.Instructions[1].MatchReturn(out var retVal)
|
|
&& retVal.MatchLdLoc(v))
|
|
{
|
|
currentField = (IField)field.MemberDefinition;
|
|
}
|
|
}
|
|
if (currentField == null)
|
|
throw new SymbolicAnalysisFailedException("Could not find currentField");
|
|
}
|
|
#endregion
|
|
|
|
#region Figure out the mapping of IEnumerable fields to IEnumerator fields (analysis of GetEnumerator())
|
|
void ResolveIEnumerableIEnumeratorFieldMapping()
|
|
{
|
|
MethodDefinitionHandle getEnumeratorMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(
|
|
m => IsMethod(m, "GetEnumerator"));
|
|
ResolveIEnumerableIEnumeratorFieldMapping(getEnumeratorMethod, context, fieldToParameterMap);
|
|
}
|
|
|
|
internal static void ResolveIEnumerableIEnumeratorFieldMapping(MethodDefinitionHandle getEnumeratorMethod, ILTransformContext context,
|
|
Dictionary<IField, ILVariable> fieldToParameterMap)
|
|
{
|
|
if (getEnumeratorMethod.IsNil)
|
|
return; // no mappings (maybe it's just an IEnumerator implementation?)
|
|
var function = CreateILAst(getEnumeratorMethod, context);
|
|
foreach (var block in function.Descendants.OfType<Block>())
|
|
{
|
|
foreach (var inst in block.Instructions)
|
|
{
|
|
// storeTarget.storeField = this.loadField;
|
|
if (inst.MatchStFld(out var storeTarget, out var storeField, out var storeValue)
|
|
&& storeValue.MatchLdFld(out var loadTarget, out var loadField)
|
|
&& loadTarget.MatchLdThis())
|
|
{
|
|
storeField = (IField)storeField.MemberDefinition;
|
|
loadField = (IField)loadField.MemberDefinition;
|
|
if (fieldToParameterMap.TryGetValue(loadField, out var mappedParameter))
|
|
fieldToParameterMap[storeField] = mappedParameter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Construction of the exception table (analysis of Dispose())
|
|
// We construct the exception table by analyzing the enumerator's Dispose() method.
|
|
|
|
void ConstructExceptionTable()
|
|
{
|
|
disposeMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(m => IsMethod(m, "Dispose"));
|
|
var function = CreateILAst(disposeMethod, context);
|
|
|
|
if (!isCompiledWithVisualBasic && !isCompiledWithMono)
|
|
{
|
|
BlockContainer body = (BlockContainer)function.Body;
|
|
foreach (var instr in body.Blocks.SelectMany(block => block.Instructions))
|
|
{
|
|
if (instr is CallInstruction call && call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis() &&
|
|
IsMethod((MethodDefinitionHandle)call.Method.MetadataToken, "MoveNext"))
|
|
{
|
|
isCompiledWithVisualBasic = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isCompiledWithMono || isCompiledWithVisualBasic)
|
|
{
|
|
BlockContainer body = (BlockContainer)function.Body;
|
|
for (var i = 0; (i < body.EntryPoint.Instructions.Count) && !(body.EntryPoint.Instructions[i] is Branch); i++)
|
|
{
|
|
if (body.EntryPoint.Instructions[i] is StObj stobj
|
|
&& stobj.MatchStFld(out var target, out var field, out var value)
|
|
&& target.MatchLdThis()
|
|
&& field.Type.IsKnownType(KnownTypeCode.Boolean)
|
|
&& value.MatchLdcI4(1))
|
|
{
|
|
disposingField = (IField)field.MemberDefinition;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// On mono and VB, we don't need to analyse Dispose() to reconstruct the try-finally structure.
|
|
finallyMethodToStateRange = default;
|
|
}
|
|
else
|
|
{
|
|
// Non-Mono/Non-VB: analyze try-finally structure in Dispose()
|
|
var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorDispose, stateField);
|
|
rangeAnalysis.AssignStateRanges(function.Body, LongSet.Universe);
|
|
finallyMethodToStateRange = rangeAnalysis.finallyMethodToStateRange;
|
|
}
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
void PrintFinallyMethodStateRanges(BlockContainer bc)
|
|
{
|
|
if (finallyMethodToStateRange == null)
|
|
return;
|
|
foreach (var (method, stateRange) in finallyMethodToStateRange)
|
|
{
|
|
bc.Blocks[0].Instructions.Insert(0, new Nop {
|
|
Comment = method.Name + " in " + stateRange
|
|
});
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Analyze MoveNext() and generate new body
|
|
BlockContainer AnalyzeMoveNext(ILFunction function)
|
|
{
|
|
context.StepStartGroup("AnalyzeMoveNext");
|
|
MethodDefinitionHandle moveNextMethod = metadata.GetTypeDefinition(enumeratorType).GetMethods().FirstOrDefault(m => metadata.GetString(metadata.GetMethodDefinition(m).Name) == "MoveNext");
|
|
ILFunction moveNextFunction = CreateILAst(moveNextMethod, context);
|
|
|
|
function.MoveNextMethod = moveNextFunction.Method;
|
|
function.SequencePointCandidates = moveNextFunction.SequencePointCandidates;
|
|
function.CodeSize = moveNextFunction.CodeSize;
|
|
function.LocalVariableSignatureLength = moveNextFunction.LocalVariableSignatureLength;
|
|
|
|
// Copy-propagate temporaries holding a copy of 'this'.
|
|
// This is necessary because the old (pre-Roslyn) C# compiler likes to store 'this' in temporary variables.
|
|
foreach (var stloc in moveNextFunction.Descendants.OfType<StLoc>().Where(s => s.Variable.IsSingleDefinition && s.Value.MatchLdThis()).ToList())
|
|
{
|
|
CopyPropagation.Propagate(stloc, context);
|
|
}
|
|
|
|
// Copy propagate stack slots holding a 32 bit integer.
|
|
foreach (var stloc in moveNextFunction.Descendants.OfType<StLoc>().Where(s => s.Variable.Kind == VariableKind.StackSlot && s.Variable.IsSingleDefinition && s.Value.OpCode == OpCode.LdcI4).ToList())
|
|
{
|
|
CopyPropagation.Propagate(stloc, context);
|
|
}
|
|
|
|
foreach (var block in moveNextFunction.Descendants.OfType<Block>())
|
|
{
|
|
block.Instructions.RemoveAll(inst => inst.OpCode == OpCode.LdcI4);
|
|
}
|
|
|
|
var body = (BlockContainer)moveNextFunction.Body;
|
|
if (body.Blocks.Count == 1 && body.Blocks[0].Instructions.Count == 1 && body.Blocks[0].Instructions[0] is TryFault tryFault)
|
|
{
|
|
body = (BlockContainer)tryFault.TryBlock;
|
|
var faultBlockContainer = tryFault.FaultBlock as BlockContainer;
|
|
if (faultBlockContainer?.Blocks.Count != 1)
|
|
throw new SymbolicAnalysisFailedException("Unexpected number of blocks in MoveNext() fault block");
|
|
var faultBlock = faultBlockContainer.Blocks.Single();
|
|
if (!(faultBlock.Instructions.Count == 2
|
|
&& faultBlock.Instructions[0] is Call call
|
|
&& call.Method.MetadataToken == disposeMethod
|
|
&& call.Arguments.Count == 1
|
|
&& call.Arguments[0].MatchLdThis()
|
|
&& faultBlock.Instructions[1].MatchLeave(faultBlockContainer)))
|
|
{
|
|
throw new SymbolicAnalysisFailedException("Unexpected fault block contents in MoveNext()");
|
|
}
|
|
}
|
|
|
|
if (isCompiledWithLegacyVisualBasic && (body.Blocks.Count == 2 || body.Blocks.Count == 1) &&
|
|
body.Blocks[0].Instructions.Count == 2 &&
|
|
body.Blocks[0].Instructions[0].MatchStLoc(out var firstVar, out var ldc) && ldc.MatchLdcI4(1))
|
|
{
|
|
doFinallyBodies = firstVar;
|
|
if (body.Blocks[0].Instructions[1] is TryCatch tryCatch && tryCatch.Handlers.Count == 1)
|
|
{
|
|
TryCatchHandler catchHandler = tryCatch.Handlers[0];
|
|
var catchBlockContainer = catchHandler.Body as BlockContainer;
|
|
if (catchBlockContainer?.Blocks.Count != 1)
|
|
throw new SymbolicAnalysisFailedException("Unexpected number of blocks in MoveNext() catch block");
|
|
var catchBlock = catchBlockContainer.Blocks.Single();
|
|
if (!(catchBlock.Instructions.Count == 4 && catchBlock.Instructions[0] is Call call &&
|
|
call.Method.Name == "SetProjectError" && call.Arguments.Count == 1 &&
|
|
call.Arguments[0].MatchLdLoc(catchHandler.Variable) &&
|
|
catchBlock.Instructions[1].MatchStLoc(out _, out var ldloc) &&
|
|
ldloc.MatchLdLoc(catchHandler.Variable) &&
|
|
catchBlock.Instructions[2].MatchStFld(out var ldThis, out var fld, out var value) &&
|
|
ldThis.MatchLdThis() && fld.MemberDefinition.Equals(stateField) && value is LdcI4 &&
|
|
catchBlock.Instructions[3] is Rethrow))
|
|
throw new SymbolicAnalysisFailedException("Unexpected catch block contents in MoveNext()");
|
|
BlockContainer tryCatchBody = (BlockContainer)tryCatch.TryBlock;
|
|
// Move return block
|
|
if (body.Blocks.Count == 2)
|
|
tryCatchBody.Blocks.Add(body.Blocks[1]);
|
|
body = tryCatchBody;
|
|
}
|
|
}
|
|
|
|
if (stateField == null)
|
|
{
|
|
// With mono-compiled state machines, it's possible that we haven't discovered the state field
|
|
// yet because the compiler let it be implicitly initialized to 0.
|
|
// In this case, we must discover it from the first instruction in MoveNext():
|
|
if (body.EntryPoint.Instructions[0] is StLoc stloc
|
|
&& stloc.Value.MatchLdFld(out var target, out var field)
|
|
&& target.MatchLdThis() && field.Type.IsKnownType(KnownTypeCode.Int32))
|
|
{
|
|
stateField = (IField)field.MemberDefinition;
|
|
}
|
|
else
|
|
{
|
|
throw new SymbolicAnalysisFailedException("Could not find state field.");
|
|
}
|
|
}
|
|
|
|
skipFinallyBodies = null;
|
|
if (isCompiledWithMono)
|
|
{
|
|
// Mono uses skipFinallyBodies; find out which variable that is:
|
|
foreach (var tryFinally in body.Descendants.OfType<TryFinally>())
|
|
{
|
|
if ((tryFinally.FinallyBlock as BlockContainer)?.EntryPoint.Instructions[0] is IfInstruction ifInst)
|
|
{
|
|
if (ifInst.Condition.MatchLogicNot(out var arg) && arg.MatchLdLoc(out var v) && v.Type.IsKnownType(KnownTypeCode.Boolean))
|
|
{
|
|
bool isInitializedInEntryBlock = false;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
if (body.EntryPoint.Instructions.ElementAtOrDefault(i) is StLoc stloc
|
|
&& stloc.Variable == v && stloc.Value.MatchLdcI4(0))
|
|
{
|
|
isInitializedInEntryBlock = true;
|
|
break;
|
|
}
|
|
}
|
|
if (isInitializedInEntryBlock)
|
|
{
|
|
skipFinallyBodies = v;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PropagateCopiesOfFields(body);
|
|
|
|
// Note: body may contain try-catch or try-finally statements that have nested block containers,
|
|
// but those cannot contain any yield statements.
|
|
// So for reconstructing the control flow, we only consider the blocks directly within body.
|
|
|
|
var rangeAnalysis = new StateRangeAnalysis(StateRangeAnalysisMode.IteratorMoveNext, stateField, legacyVisualBasic: isCompiledWithLegacyVisualBasic);
|
|
rangeAnalysis.skipFinallyBodies = skipFinallyBodies;
|
|
rangeAnalysis.doFinallyBodies = doFinallyBodies;
|
|
rangeAnalysis.CancellationToken = context.CancellationToken;
|
|
rangeAnalysis.AssignStateRanges(body, LongSet.Universe);
|
|
cachedStateVars = rangeAnalysis.CachedStateVars.ToHashSet();
|
|
|
|
var newBody = ConvertBody(body, rangeAnalysis);
|
|
moveNextFunction.Variables.Clear();
|
|
// release references from old moveNextFunction to instructions that were moved over to newBody
|
|
moveNextFunction.ReleaseRef();
|
|
context.StepEndGroup();
|
|
return newBody;
|
|
}
|
|
|
|
private void PropagateCopiesOfFields(BlockContainer body)
|
|
{
|
|
// Roslyn may optimize MoveNext() by copying fields from the iterator class into local variables
|
|
// at the beginning of MoveNext(). Undo this optimization.
|
|
context.StepStartGroup("PropagateCopiesOfFields");
|
|
var mutableFields = body.Descendants.OfType<LdFlda>().Where(ldflda => ldflda.Parent.OpCode != OpCode.LdObj).Select(ldflda => ldflda.Field).ToHashSet();
|
|
for (int i = 0; i < body.EntryPoint.Instructions.Count; i++)
|
|
{
|
|
if (body.EntryPoint.Instructions[i] is StLoc store
|
|
&& store.Variable.IsSingleDefinition
|
|
&& store.Value is LdObj ldobj
|
|
&& ldobj.Target is LdFlda ldflda
|
|
&& ldflda.Target.MatchLdThis())
|
|
{
|
|
if (!mutableFields.Contains(ldflda.Field))
|
|
{
|
|
// perform copy propagation: (unlike CopyPropagation.Propagate(), copy the ldobj arguments as well)
|
|
foreach (var expr in store.Variable.LoadInstructions.ToArray())
|
|
{
|
|
expr.ReplaceWith(store.Value.Clone());
|
|
}
|
|
body.EntryPoint.Instructions.RemoveAt(i--);
|
|
}
|
|
else if (ldflda.Field.MemberDefinition == stateField.MemberDefinition)
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
break; // unsupported: load of mutable field (other than state field)
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break; // unknown instruction
|
|
}
|
|
}
|
|
context.StepEndGroup();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert the old body (of MoveNext function) to the new body (of decompiled iterator method).
|
|
///
|
|
/// * Replace the sequence
|
|
/// this.currentField = expr;
|
|
/// this.state = N;
|
|
/// return true;
|
|
/// with:
|
|
/// yield return expr;
|
|
/// goto blockForState(N);
|
|
/// * Replace the sequence:
|
|
/// this._finally2();
|
|
/// this._finally1();
|
|
/// return false;
|
|
/// with:
|
|
/// yield break;
|
|
/// * Reconstruct try-finally blocks from
|
|
/// (on enter) this.state = N;
|
|
/// (on exit) this._finallyX();
|
|
/// </summary>
|
|
private BlockContainer ConvertBody(BlockContainer oldBody, StateRangeAnalysis rangeAnalysis)
|
|
{
|
|
var blockStateMap = rangeAnalysis.GetBlockStateSetMapping(oldBody);
|
|
BlockContainer newBody = new BlockContainer().WithILRange(oldBody);
|
|
// create all new blocks so that they can be referenced by gotos
|
|
for (int blockIndex = 0; blockIndex < oldBody.Blocks.Count; blockIndex++)
|
|
{
|
|
newBody.Blocks.Add(new Block().WithILRange(oldBody.Blocks[blockIndex]));
|
|
}
|
|
// convert contents of blocks
|
|
|
|
for (int i = 0; i < oldBody.Blocks.Count; i++)
|
|
{
|
|
var oldBlock = oldBody.Blocks[i];
|
|
var newBlock = newBody.Blocks[i];
|
|
foreach (var oldInst in oldBlock.Instructions)
|
|
{
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
if (oldInst.MatchStFld(out var target, out var field, out var value) && target.MatchLdThis())
|
|
{
|
|
if (field.MemberDefinition.Equals(stateField))
|
|
{
|
|
if (value.MatchLdcI4(out int newState))
|
|
{
|
|
// On state change, break up the block:
|
|
// (this allows us to consider each block individually for try-finally reconstruction)
|
|
newBlock = SplitBlock(newBlock, oldInst);
|
|
// We keep the state-changing instruction around (as first instruction of the new block)
|
|
// for reconstructing the try-finallys.
|
|
}
|
|
else
|
|
{
|
|
newBlock.Instructions.Add(new InvalidExpression("Assigned non-constant to iterator.state field").WithILRange(oldInst));
|
|
continue; // don't copy over this instruction, but continue with the basic block
|
|
}
|
|
}
|
|
else if (field.MemberDefinition.Equals(currentField))
|
|
{
|
|
// create yield return
|
|
newBlock.Instructions.Add(new YieldReturn(value).WithILRange(oldInst));
|
|
ConvertBranchAfterYieldReturn(newBlock, oldBlock, oldInst.ChildIndex + 1);
|
|
break; // we're done with this basic block
|
|
}
|
|
}
|
|
else if (oldInst is Call call && call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
|
|
&& finallyMethodToStateRange.ContainsKey((IMethod)call.Method.MemberDefinition))
|
|
{
|
|
// Break up the basic block on a call to a finally method
|
|
// (this allows us to consider each block individually for try-finally reconstruction)
|
|
newBlock = SplitBlock(newBlock, oldInst);
|
|
}
|
|
else if (oldInst is TryFinally tryFinally && (isCompiledWithMono || isCompiledWithVisualBasic))
|
|
{
|
|
// with mono, we have to recurse into try-finally blocks
|
|
var oldTryBlock = (BlockContainer)tryFinally.TryBlock;
|
|
var sra = rangeAnalysis.CreateNestedAnalysis();
|
|
sra.AssignStateRanges(oldTryBlock, LongSet.Universe);
|
|
tryFinally.TryBlock = ConvertBody(oldTryBlock, sra);
|
|
}
|
|
else if (isCompiledWithLegacyVisualBasic && oldInst is IfInstruction ifInstruction &&
|
|
ifInstruction.FalseInst.MatchNop() &&
|
|
ifInstruction.Condition.MatchCompEquals(out var left, out var right) &&
|
|
left.MatchLdFld(out var ldThis, out var fld) && ldThis.MatchLdThis() &&
|
|
fld.MemberDefinition.Equals(disposingField) &&
|
|
right.MatchLdcI4(0))
|
|
{
|
|
newBlock.Instructions.Add(ifInstruction.TrueInst);
|
|
newBlock.AddILRange(ifInstruction.TrueInst);
|
|
UpdateBranchTargets(ifInstruction.TrueInst);
|
|
break;
|
|
}
|
|
|
|
// copy over the instruction to the new block
|
|
newBlock.Instructions.Add(oldInst);
|
|
newBlock.AddILRange(oldInst);
|
|
UpdateBranchTargets(oldInst);
|
|
}
|
|
}
|
|
|
|
// Insert new artificial block as entry point, and jump to the initial state.
|
|
// This causes the method to start directly at the first user code,
|
|
// and the whole compiler-generated state-dispatching logic becomes unreachable code
|
|
// and gets deleted.
|
|
int initialState = isCompiledWithLegacyVisualBasic ? -1 : 0;
|
|
newBody.Blocks.Insert(0, new Block {
|
|
Instructions = { MakeGoTo(initialState) }
|
|
});
|
|
return newBody;
|
|
|
|
void ConvertBranchAfterYieldReturn(Block newBlock, Block oldBlock, int pos)
|
|
{
|
|
Block targetBlock;
|
|
if (isCompiledWithMono && disposingField != null)
|
|
{
|
|
// Mono skips over the state assignment if 'this.disposing' is set:
|
|
// ...
|
|
// stfld $current(ldloc this, yield-expr)
|
|
// if (ldfld $disposing(ldloc this)) br IL_007c
|
|
// br targetBlock
|
|
// }
|
|
//
|
|
// Block targetBlock (incoming: 1) {
|
|
// stfld $PC(ldloc this, ldc.i4 1)
|
|
// br setSkipFinallyBodies
|
|
// }
|
|
//
|
|
// Block setSkipFinallyBodies (incoming: 2) {
|
|
// stloc skipFinallyBodies(ldc.i4 1)
|
|
// br returnBlock
|
|
// }
|
|
if (oldBlock.Instructions[pos].MatchIfInstruction(out var condition, out _)
|
|
&& condition.MatchLdFld(out var condTarget, out var condField)
|
|
&& condTarget.MatchLdThis() && condField.MemberDefinition.Equals(disposingField)
|
|
&& oldBlock.Instructions[pos + 1].MatchBranch(out targetBlock)
|
|
&& targetBlock.Parent == oldBlock.Parent)
|
|
{
|
|
// Keep looking at the target block:
|
|
oldBlock = targetBlock;
|
|
pos = 0;
|
|
}
|
|
}
|
|
|
|
// Visual Basic Compiler emits additional stores to variables.
|
|
int? localNewState = null;
|
|
if (oldBlock.Instructions[pos].MatchStLoc(out _, out var value) && value is LdcI4 ldci4)
|
|
{
|
|
localNewState = ldci4.Value;
|
|
pos++;
|
|
}
|
|
|
|
if (oldBlock.Instructions[pos].MatchStFld(out var target, out var field, out value)
|
|
&& target.MatchLdThis()
|
|
&& field.MemberDefinition == stateField
|
|
&& value.MatchLdcI4(out int newState)
|
|
&& (localNewState is null || localNewState == newState))
|
|
{
|
|
pos++;
|
|
}
|
|
else
|
|
{
|
|
newBlock.Instructions.Add(new InvalidBranch("Unable to find new state assignment for yield return"));
|
|
return;
|
|
}
|
|
// Mono may have 'br setSkipFinallyBodies' here, so follow the branch
|
|
if (oldBlock.Instructions[pos].MatchBranch(out targetBlock) && targetBlock.Parent == oldBlock.Parent)
|
|
{
|
|
oldBlock = targetBlock;
|
|
pos = 0;
|
|
}
|
|
if (oldBlock.Instructions[pos].MatchStLoc(skipFinallyBodies, out value))
|
|
{
|
|
if (!value.MatchLdcI4(1))
|
|
{
|
|
newBlock.Instructions.Add(new InvalidExpression {
|
|
ExpectedResultType = StackType.Void,
|
|
Message = "Unexpected assignment to skipFinallyBodies"
|
|
});
|
|
}
|
|
pos++;
|
|
}
|
|
// We can't use MatchStLoc like above since the doFinallyBodies variable is split by SplitVariables.
|
|
// This occurs for the Legacy VBC compiler.
|
|
if (oldBlock.Instructions[pos].MatchStLoc(out var var, out value) && var.Kind == VariableKind.Local && var.Index == doFinallyBodies?.Index)
|
|
{
|
|
if (!value.MatchLdcI4(0))
|
|
{
|
|
newBlock.Instructions.Add(new InvalidExpression {
|
|
ExpectedResultType = StackType.Void,
|
|
Message = "Unexpected assignment to doFinallyBodies"
|
|
});
|
|
}
|
|
pos++;
|
|
}
|
|
|
|
if (oldBlock.Instructions[pos].MatchReturn(out var retVal) && retVal.MatchLdcI4(1))
|
|
{
|
|
// OK, found return directly after state assignment
|
|
}
|
|
else if (oldBlock.Instructions[pos].MatchBranch(out targetBlock)
|
|
&& targetBlock.Instructions[0].MatchReturn(out retVal) && retVal.MatchLdcI4(1))
|
|
{
|
|
// OK, jump to common return block (e.g. on Mono)
|
|
}
|
|
else
|
|
{
|
|
newBlock.Instructions.Add(new InvalidBranch("Unable to find 'return true' for yield return"));
|
|
return;
|
|
}
|
|
newBlock.Instructions.Add(MakeGoTo(newState));
|
|
}
|
|
|
|
Block SplitBlock(Block newBlock, ILInstruction oldInst)
|
|
{
|
|
if (newBlock.Instructions.Count > 0)
|
|
{
|
|
var newBlock2 = new Block();
|
|
newBlock2.AddILRange(new Interval(oldInst.StartILOffset, oldInst.StartILOffset));
|
|
newBody.Blocks.Add(newBlock2);
|
|
newBlock.Instructions.Add(new Branch(newBlock2));
|
|
newBlock = newBlock2;
|
|
}
|
|
return newBlock;
|
|
}
|
|
|
|
ILInstruction MakeGoTo(int v)
|
|
{
|
|
Block targetBlock = blockStateMap.GetOrDefault(v);
|
|
if (targetBlock != null)
|
|
{
|
|
if (targetBlock.Parent == oldBody)
|
|
return new Branch(newBody.Blocks[targetBlock.ChildIndex]);
|
|
else
|
|
return new Branch(targetBlock);
|
|
}
|
|
else
|
|
{
|
|
return new InvalidBranch("Could not find block for state " + v);
|
|
}
|
|
}
|
|
|
|
void UpdateBranchTargets(ILInstruction inst)
|
|
{
|
|
switch (inst)
|
|
{
|
|
case Branch branch:
|
|
if (branch.TargetContainer == oldBody)
|
|
{
|
|
branch.TargetBlock = newBody.Blocks[branch.TargetBlock.ChildIndex];
|
|
}
|
|
break;
|
|
case Leave leave:
|
|
if (leave.MatchReturn(out var value))
|
|
{
|
|
bool validYieldBreak = value.MatchLdcI4(0);
|
|
if (value.MatchLdLoc(out var v)
|
|
&& (v.Kind == VariableKind.Local || v.Kind == VariableKind.StackSlot)
|
|
&& v.StoreInstructions.All(store => store is StLoc stloc && stloc.Value.MatchLdcI4(0)))
|
|
{
|
|
validYieldBreak = true;
|
|
returnStores.AddRange(v.StoreInstructions.Cast<StLoc>());
|
|
}
|
|
if (validYieldBreak)
|
|
{
|
|
// yield break
|
|
leave.ReplaceWith(new Leave(newBody).WithILRange(leave));
|
|
}
|
|
else
|
|
{
|
|
leave.ReplaceWith(new InvalidBranch("Unexpected return in MoveNext()").WithILRange(leave));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (leave.TargetContainer == oldBody)
|
|
{
|
|
leave.TargetContainer = newBody;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
foreach (var child in inst.Children)
|
|
{
|
|
UpdateBranchTargets(child);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region TranslateFieldsToLocalAccess
|
|
/// <summary>
|
|
/// Translates all field accesses in `function` to local variable accesses.
|
|
/// </summary>
|
|
internal static void TranslateFieldsToLocalAccess(ILFunction function, ILInstruction inst, Dictionary<IField, ILVariable> fieldToVariableMap, bool isCompiledWithMono = false)
|
|
{
|
|
if (inst is LdFlda ldflda && ldflda.Target.MatchLdThis())
|
|
{
|
|
var fieldDef = (IField)ldflda.Field.MemberDefinition;
|
|
if (!fieldToVariableMap.TryGetValue(fieldDef, out var v))
|
|
{
|
|
string name = null;
|
|
if (!string.IsNullOrEmpty(fieldDef.Name) && fieldDef.Name[0] == '<')
|
|
{
|
|
int pos = fieldDef.Name.IndexOf('>');
|
|
if (pos > 1)
|
|
name = fieldDef.Name.Substring(1, pos - 1);
|
|
}
|
|
v = function.RegisterVariable(VariableKind.Local, ldflda.Field.ReturnType, name);
|
|
v.InitialValueIsInitialized = true; // the field was default-initialized, so keep those semantics for the variable
|
|
v.UsesInitialValue = true;
|
|
v.StateMachineField = ldflda.Field;
|
|
fieldToVariableMap.Add(fieldDef, v);
|
|
}
|
|
if (v.StackType == StackType.Ref)
|
|
{
|
|
Debug.Assert(v.Kind == VariableKind.Parameter && v.Index < 0); // this pointer
|
|
inst.ReplaceWith(new LdLoc(v).WithILRange(inst));
|
|
}
|
|
else
|
|
{
|
|
inst.ReplaceWith(new LdLoca(v).WithILRange(inst));
|
|
}
|
|
}
|
|
else if (!isCompiledWithMono && inst.MatchLdThis())
|
|
{
|
|
inst.ReplaceWith(new InvalidExpression("stateMachine") { ExpectedResultType = inst.ResultType }.WithILRange(inst));
|
|
}
|
|
else
|
|
{
|
|
foreach (var child in inst.Children)
|
|
{
|
|
TranslateFieldsToLocalAccess(function, child, fieldToVariableMap, isCompiledWithMono);
|
|
}
|
|
if (inst is LdObj ldobj && ldobj.Target is LdLoca ldloca && ldloca.Variable.StateMachineField != null)
|
|
{
|
|
LdLoc ldloc = new LdLoc(ldloca.Variable);
|
|
ldloc.AddILRange(ldobj);
|
|
ldloc.AddILRange(ldloca);
|
|
inst.ReplaceWith(ldloc);
|
|
}
|
|
else if (inst is StObj stobj && stobj.Target is LdLoca ldloca2 && ldloca2.Variable.StateMachineField != null)
|
|
{
|
|
StLoc stloc = new StLoc(ldloca2.Variable, stobj.Value);
|
|
stloc.AddILRange(stobj);
|
|
stloc.AddILRange(ldloca2);
|
|
inst.ReplaceWith(stloc);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region DecompileFinallyBlocks
|
|
void DecompileFinallyBlocks()
|
|
{
|
|
foreach (var method in finallyMethodToStateRange.Keys)
|
|
{
|
|
var function = CreateILAst((MethodDefinitionHandle)method.MetadataToken, context);
|
|
var body = (BlockContainer)function.Body;
|
|
var newState = GetNewState(body.EntryPoint);
|
|
if (newState != null)
|
|
{
|
|
body.EntryPoint.Instructions.RemoveAt(0);
|
|
}
|
|
// Avoid yield-return decompilation if there are unrecognized state assignments in a finally method.
|
|
foreach (var inst in body.Descendants)
|
|
{
|
|
if (IsStateAssignment(inst))
|
|
throw new SymbolicAnalysisFailedException($"Unknown state transition in {function.Name ?? "finally"} at IL_{inst.StartILOffset:x4}");
|
|
}
|
|
function.ReleaseRef(); // make body reusable outside of function
|
|
decompiledFinallyMethods.Add(method, (newState, function));
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Reconstruct try-finally blocks
|
|
|
|
/// <summary>
|
|
/// Reconstruct try-finally blocks.
|
|
/// * The stateChanges (iterator._state = N;) tell us when to open a try-finally block
|
|
/// * The calls to the finally method tell us when to leave the try block.
|
|
///
|
|
/// There might be multiple stateChanges for a given try-finally block, e.g.
|
|
/// both the original entry point, and the target when leaving a nested block.
|
|
/// In proper C# code, the entry point of the try-finally will dominate all other code
|
|
/// in the try-block, so we can use dominance to find the proper entry point.
|
|
///
|
|
/// Precondition: the blocks in newBody are topologically sorted.
|
|
/// </summary>
|
|
void ReconstructTryFinallyBlocks(ILFunction iteratorFunction)
|
|
{
|
|
BlockContainer newBody = (BlockContainer)iteratorFunction.Body;
|
|
context.Step("Reconstuct try-finally blocks", newBody);
|
|
var blockState = new int[newBody.Blocks.Count];
|
|
blockState[0] = -1;
|
|
var stateToContainer = new Dictionary<int, BlockContainer>();
|
|
stateToContainer.Add(-1, newBody);
|
|
// First, analyse the newBody: for each block, determine the active state number.
|
|
foreach (var block in newBody.Blocks)
|
|
{
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
int oldState = blockState[block.ChildIndex];
|
|
BlockContainer container; // new container for the block
|
|
if (GetNewState(block) is int newState)
|
|
{
|
|
// OK, state change
|
|
// Remove the state-changing instruction
|
|
block.Instructions.RemoveAt(0);
|
|
|
|
if (!stateToContainer.TryGetValue(newState, out container))
|
|
{
|
|
// First time we see this state.
|
|
// This means we just found the entry point of a try block.
|
|
CreateTryBlock(block, newState);
|
|
// CreateTryBlock() wraps the contents of 'block' with a TryFinally.
|
|
// We thus need to put the block (which now contains the whole TryFinally)
|
|
// into the parent container.
|
|
// Assuming a state transition never enters more than one state at once,
|
|
// we can use stateToContainer[oldState] as parent.
|
|
container = stateToContainer[oldState];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Because newBody is topologically sorted we because we just removed unreachable code,
|
|
// we can assume that blockState[] was already set for this block.
|
|
newState = oldState;
|
|
container = stateToContainer[oldState];
|
|
}
|
|
if (container != newBody)
|
|
{
|
|
// Move the block into the container.
|
|
container.Blocks.Add(block);
|
|
// Keep the stale reference in newBody.Blocks for now, to avoid
|
|
// changing the ChildIndex of the other blocks while we use it
|
|
// to index the blockState array.
|
|
}
|
|
#if DEBUG
|
|
block.Instructions.Insert(0, new Nop { Comment = "state == " + newState });
|
|
#endif
|
|
// Propagate newState to successor blocks
|
|
foreach (var branch in block.Descendants.OfType<Branch>())
|
|
{
|
|
if (branch.TargetBlock.Parent == newBody)
|
|
{
|
|
int stateAfterBranch = newState;
|
|
if (Block.GetPredecessor(branch) is Call call
|
|
&& call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
|
|
&& call.Method.Name == "System.IDisposable.Dispose")
|
|
{
|
|
// pre-roslyn compiles "yield break;" into "Dispose(); goto return_false;",
|
|
// so convert the dispose call into a state transition to the final state
|
|
stateAfterBranch = -1;
|
|
call.ReplaceWith(new Nop() { Comment = "Dispose call" });
|
|
}
|
|
Debug.Assert(blockState[branch.TargetBlock.ChildIndex] == stateAfterBranch || blockState[branch.TargetBlock.ChildIndex] == 0);
|
|
blockState[branch.TargetBlock.ChildIndex] = stateAfterBranch;
|
|
}
|
|
}
|
|
}
|
|
newBody.Blocks.RemoveAll(b => b.Parent != newBody);
|
|
|
|
void CreateTryBlock(Block block, int state)
|
|
{
|
|
var finallyMethod = FindFinallyMethod(state);
|
|
if (finallyMethod != null)
|
|
{
|
|
// remove the method so that it doesn't cause ambiguity when processing nested try-finally blocks
|
|
finallyMethodToStateRange.Remove(finallyMethod);
|
|
}
|
|
|
|
var tryBlock = new Block();
|
|
tryBlock.AddILRange(block);
|
|
tryBlock.Instructions.AddRange(block.Instructions);
|
|
var tryBlockContainer = new BlockContainer();
|
|
tryBlockContainer.Blocks.Add(tryBlock);
|
|
tryBlockContainer.AddILRange(tryBlock);
|
|
stateToContainer.Add(state, tryBlockContainer);
|
|
|
|
ILInstruction finallyBlock;
|
|
if (finallyMethod == null)
|
|
{
|
|
finallyBlock = new InvalidBranch($"Could not find finallyMethod for state={state}.\n" +
|
|
$"Possibly this method is affected by a C# compiler bug that causes the finally body\n" +
|
|
$"not to run in case of an exception or early 'break;' out of a loop consuming this iterable.");
|
|
}
|
|
else if (decompiledFinallyMethods.TryGetValue(finallyMethod, out var decompiledMethod))
|
|
{
|
|
finallyBlock = decompiledMethod.function.Body;
|
|
var vars = decompiledMethod.function.Variables.ToArray();
|
|
decompiledMethod.function.Variables.Clear();
|
|
iteratorFunction.Variables.AddRange(vars);
|
|
}
|
|
else
|
|
{
|
|
finallyBlock = new InvalidBranch("Missing decompiledFinallyMethod");
|
|
}
|
|
|
|
block.Instructions.Clear();
|
|
block.Instructions.Add(new TryFinally(tryBlockContainer, finallyBlock).WithILRange(tryBlockContainer));
|
|
}
|
|
|
|
IMethod FindFinallyMethod(int state)
|
|
{
|
|
IMethod foundMethod = null;
|
|
foreach (var (method, stateRange) in finallyMethodToStateRange)
|
|
{
|
|
if (stateRange.Contains(state))
|
|
{
|
|
if (foundMethod == null)
|
|
foundMethod = method;
|
|
else
|
|
Debug.Fail("Ambiguous finally method for state " + state);
|
|
}
|
|
}
|
|
return foundMethod;
|
|
}
|
|
}
|
|
|
|
bool IsStateAssignment(ILInstruction inst)
|
|
{
|
|
return inst.MatchStFld(out var target, out var field, out _)
|
|
&& target.MatchLdThis()
|
|
&& field.MemberDefinition.Equals(stateField);
|
|
}
|
|
|
|
// Gets the state that is transitioned to at the start of the block
|
|
int? GetNewState(Block block)
|
|
{
|
|
if (block.Instructions[0].MatchStFld(out var target, out var field, out var value)
|
|
&& target.MatchLdThis()
|
|
&& field.MemberDefinition.Equals(stateField)
|
|
&& value.MatchLdcI4(out int newState))
|
|
{
|
|
return newState;
|
|
}
|
|
else if (block.Instructions[0] is Call call
|
|
&& call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()
|
|
&& decompiledFinallyMethods.TryGetValue((IMethod)call.Method.MemberDefinition, out var finallyMethod))
|
|
{
|
|
return finallyMethod.outerState;
|
|
}
|
|
return null;
|
|
}
|
|
#endregion
|
|
|
|
#region Cleanup finally blocks
|
|
|
|
/// <summary>
|
|
/// Eliminates usage of doFinallyBodies
|
|
/// </summary>
|
|
private void CleanSkipFinallyBodies(ILFunction function)
|
|
{
|
|
if (skipFinallyBodies == null)
|
|
{
|
|
return; // only mono-compiled code uses skipFinallyBodies
|
|
}
|
|
context.StepStartGroup("CleanFinallyBlocks", function);
|
|
if (skipFinallyBodies.StoreInstructions.Count != 0 || skipFinallyBodies.AddressCount != 0)
|
|
{
|
|
// misdetected another variable as skipFinallyBodies?
|
|
// Fortunately removing the initial store of 0 is harmless, as we
|
|
// default-initialize the variable on uninit uses
|
|
return;
|
|
}
|
|
foreach (var tryFinally in function.Descendants.OfType<TryFinally>())
|
|
{
|
|
if (!(tryFinally.FinallyBlock is BlockContainer container))
|
|
continue;
|
|
Block entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(container);
|
|
if (entryPoint?.Instructions[0] is IfInstruction ifInst)
|
|
{
|
|
if (ifInst.Condition.MatchLogicNot(out var logicNotArg) && logicNotArg.MatchLdLoc(skipFinallyBodies))
|
|
{
|
|
context.Step("Remove if (skipFinallyBodies) from try-finally", tryFinally);
|
|
// condition will always be true now that we're using 'yield' instructions
|
|
entryPoint.Instructions[0] = ifInst.TrueInst;
|
|
entryPoint.Instructions.RemoveRange(1, entryPoint.Instructions.Count - 1);
|
|
// find new entryPoint after the old one was modified
|
|
entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(container);
|
|
}
|
|
}
|
|
if (entryPoint?.Instructions.Count == 2
|
|
&& IsCallToMonoFinallyMethod(entryPoint.Instructions[0] as Call, out var finallyMethod)
|
|
&& entryPoint.Instructions[1].MatchLeave((BlockContainer)tryFinally.FinallyBlock))
|
|
{
|
|
context.Step("Inline " + finallyMethod.FullName + " into finally", tryFinally);
|
|
var finallyFunction = CreateILAst((MethodDefinitionHandle)finallyMethod.MetadataToken, context);
|
|
tryFinally.FinallyBlock = finallyFunction.Body;
|
|
var variables = finallyFunction.Variables.ToArray();
|
|
finallyFunction.Variables.Clear();
|
|
function.Variables.AddRange(variables);
|
|
}
|
|
}
|
|
context.StepEndGroup(keepIfEmpty: true);
|
|
|
|
bool IsCallToMonoFinallyMethod(Call call, out IMethod finallyMethod)
|
|
{
|
|
finallyMethod = default;
|
|
if (call == null)
|
|
return false;
|
|
if (!(call.Arguments.Count == 1 && call.Arguments[0].MatchLdThis()))
|
|
return false;
|
|
if (!call.Method.Name.StartsWith("<>__Finally"))
|
|
return false;
|
|
ITypeDefinition declaringTypeDefinition = call.Method.DeclaringTypeDefinition;
|
|
if (declaringTypeDefinition.MetadataToken != this.enumeratorType)
|
|
return false;
|
|
if (declaringTypeDefinition.ParentModule.MetadataFile.Metadata != metadata)
|
|
return false;
|
|
finallyMethod = call.Method;
|
|
return !call.Method.MetadataToken.IsNil;
|
|
}
|
|
}
|
|
|
|
private void CleanDoFinallyBodies(ILFunction function)
|
|
{
|
|
if (doFinallyBodies == null)
|
|
{
|
|
return; // only VB code uses doFinallyBodies
|
|
}
|
|
context.StepStartGroup("CleanDoFinallyBodies", function);
|
|
if (doFinallyBodies.StoreInstructions.Count != 0 || doFinallyBodies.AddressCount != 0)
|
|
{
|
|
// misdetected another variable as skipFinallyBodies?
|
|
// Fortunately removing the initial store of 0 is harmless, as we
|
|
// default-initialize the variable on uninit uses
|
|
return;
|
|
}
|
|
foreach (var tryFinally in function.Descendants.OfType<TryFinally>())
|
|
{
|
|
if (!(tryFinally.FinallyBlock is BlockContainer container))
|
|
continue;
|
|
Block entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(container);
|
|
if (entryPoint?.Instructions[0] is IfInstruction ifInst)
|
|
{
|
|
if (ifInst.Condition.MatchCompEquals(out var left, out var right) && left.MatchLdLoc(doFinallyBodies) && right.MatchLdcI4(0))
|
|
{
|
|
context.Step("Remove if (doFinallyBodies) from try-finally", tryFinally);
|
|
// condition will always be false now that we're using 'yield' instructions
|
|
entryPoint.Instructions.RemoveAt(0);
|
|
}
|
|
}
|
|
}
|
|
foreach (LdLoc load in doFinallyBodies.LoadInstructions.ToArray())
|
|
{
|
|
load.ReplaceWith(new LdcI4(1).WithILRange(load));
|
|
}
|
|
context.StepEndGroup(keepIfEmpty: true);
|
|
}
|
|
|
|
private void CleanFinallyStateChecks(ILFunction function)
|
|
{
|
|
context.StepStartGroup("CleanFinallyStateChecks", function);
|
|
foreach (var tryFinally in function.Descendants.OfType<TryFinally>())
|
|
{
|
|
if (!(tryFinally.FinallyBlock is BlockContainer container))
|
|
continue;
|
|
Block entryPoint = AsyncAwaitDecompiler.GetBodyEntryPoint(container);
|
|
if (entryPoint?.Instructions[0] is IfInstruction ifInst)
|
|
{
|
|
if (ifInst.Condition is Comp comp && comp.Kind == ComparisonKind.GreaterThanOrEqual &&
|
|
comp.InputType == StackType.I4 && comp.Sign == Sign.Signed && comp.Left.MatchLdLoc(out var variable) &&
|
|
cachedStateVars.Contains(variable) &&
|
|
comp.Right.MatchLdcI4(0))
|
|
{
|
|
context.Step("Remove if (stateVar >= 0) from try-finally", tryFinally);
|
|
// condition will always be false now that we're using 'yield' instructions
|
|
entryPoint.Instructions.RemoveAt(0);
|
|
}
|
|
}
|
|
}
|
|
context.StepEndGroup(keepIfEmpty: true);
|
|
}
|
|
|
|
#endregion
|
|
|
|
bool IsMethod(MethodDefinitionHandle method, string name)
|
|
{
|
|
var methodDefinition = metadata.GetMethodDefinition(method);
|
|
if (metadata.GetString(methodDefinition.Name) == name)
|
|
return true;
|
|
foreach (var implHandle in method.GetMethodImplementations(metadata))
|
|
{
|
|
var impl = metadata.GetMethodImplementation(implHandle);
|
|
switch (impl.MethodDeclaration.Kind)
|
|
{
|
|
case HandleKind.MethodDefinition:
|
|
var md = metadata.GetMethodDefinition((MethodDefinitionHandle)impl.MethodDeclaration);
|
|
if (metadata.GetString(md.Name) != name)
|
|
continue;
|
|
return true;
|
|
case HandleKind.MemberReference:
|
|
var mr = metadata.GetMemberReference((MemberReferenceHandle)impl.MethodDeclaration);
|
|
if (mr.GetKind() != MemberReferenceKind.Method)
|
|
continue;
|
|
if (metadata.GetString(mr.Name) != name)
|
|
continue;
|
|
return true;
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|