// Copyright (c) 2016 Daniel Grunwald // // 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.Collections.Generic; using System.Linq; using ICSharpCode.Decompiler.FlowAnalysis; using ICSharpCode.Decompiler.Util; using System.Threading; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.IL.Transforms { /// /// Live range splitting for IL variables. /// public class SplitVariables : IILTransform { public void Run(ILFunction function, ILTransformContext context) { var groupStores = new GroupStores(function, context.CancellationToken); function.Body.AcceptVisitor(groupStores); // Replace analyzed variables with their split versions: foreach (var inst in function.Descendants.OfType()) { if (groupStores.IsAnalyzedVariable(inst.Variable)) { inst.Variable = groupStores.GetNewVariable(inst); } } function.Variables.RemoveDead(); } static bool IsCandidateVariable(ILVariable v) { switch (v.Kind) { case VariableKind.Local: foreach (var ldloca in v.AddressInstructions) { if (DetermineAddressUse(ldloca, ldloca.Variable) == AddressUse.Unknown) { // If we don't understand how the address is being used, // we can't split the variable. return false; } } return true; case VariableKind.StackSlot: // stack slots: are already split by construction, // except for the locals-turned-stackslots in async functions if (v.Function.IsAsync) goto case VariableKind.Local; else return false; default: // parameters: avoid splitting parameters // pinned locals: must not split (doing so would extend the life of the pin to the end of the method) return false; } } enum AddressUse { Unknown, /// /// Address is immediately used for reading and/or writing, /// without the possibility of the variable being directly stored to (via 'stloc') /// in between the 'ldloca' and the use of the address. /// Immediate, /// /// We support some limited form of ref locals referring to a target variable, /// without giving up splitting of the target variable. /// Requirements: /// * the ref local is single-assignment /// * the ref local is initialized directly with 'ldloca target; stloc ref_local', /// not a derived pointer (e.g. 'ldloca target; ldflda F; stloc ref_local'). /// * all uses of the ref_local are immediate. /// There may be stores to the target variable in between the 'stloc ref_local' and its uses, /// but we handle that case by treating each use of the ref_local as an address access /// of the target variable (as if the ref_local was eliminated via copy propagation). /// WithSupportedRefLocals, } static AddressUse DetermineAddressUse(ILInstruction addressLoadingInstruction, ILVariable targetVar) { switch (addressLoadingInstruction.Parent) { case LdObj ldobj: return AddressUse.Immediate; case LdFlda ldflda: return DetermineAddressUse(ldflda, targetVar); case Await await: // GetAwaiter() may write to the struct, but shouldn't store the address for later use return AddressUse.Immediate; case CallInstruction call: return HandleCall(addressLoadingInstruction, targetVar, call); case StLoc stloc when stloc.Variable.IsSingleDefinition: // Address stored in local variable: also check all uses of that variable. if (!(stloc.Variable.Kind == VariableKind.StackSlot || stloc.Variable.Kind == VariableKind.Local)) return AddressUse.Unknown; var value = stloc.Value; while (value is LdFlda ldFlda) { value = ldFlda.Target; } if (value.OpCode != OpCode.LdLoca) { // GroupStores only handles ref-locals correctly when they are supported by GetAddressLoadForRefLocalUse(), // which only works for ldflda*(ldloca) return AddressUse.Unknown; } foreach (var load in stloc.Variable.LoadInstructions) { if (DetermineAddressUse(load, targetVar) != AddressUse.Immediate) return AddressUse.Unknown; } return AddressUse.WithSupportedRefLocals; default: return AddressUse.Unknown; } } static AddressUse HandleCall(ILInstruction addressLoadingInstruction, ILVariable targetVar, CallInstruction call) { // Address is passed to method. // We'll assume the method only uses the address locally, // unless we can see an address being returned from the method: IType returnType = (call is NewObj) ? call.Method.DeclaringType : call.Method.ReturnType; if (returnType.IsByRefLike) { // If the address is returned from the method, it check whether it's consumed immediately. // This can still be fine, as long as we also check the consumer's other arguments for 'stloc targetVar'. if (DetermineAddressUse(call, targetVar) != AddressUse.Immediate) return AddressUse.Unknown; } foreach (var p in call.Method.Parameters) { // catch "out Span" and similar if (p.Type.SkipModifiers() is ByReferenceType brt && brt.ElementType.IsByRefLike) return AddressUse.Unknown; } // ensure there's no 'stloc target' in between the ldloca and the call consuming the address for (int i = addressLoadingInstruction.ChildIndex + 1; i < call.Arguments.Count; i++) { foreach (var inst in call.Arguments[i].Descendants) { if (inst is StLoc store && store.Variable == targetVar) return AddressUse.Unknown; } } return AddressUse.Immediate; } /// /// Given 'ldloc ref_local' and 'ldloca target; stloc ref_local', returns the ldloca. /// This function must return a non-null LdLoca for every use of a SupportedRefLocal. /// static LdLoca GetAddressLoadForRefLocalUse(LdLoc ldloc) { if (!ldloc.Variable.IsSingleDefinition) return null; // only single-definition variables can be supported ref locals var store = ldloc.Variable.StoreInstructions.SingleOrDefault(); if (store is StLoc stloc) { var value = stloc.Value; while (value is LdFlda ldFlda) { value = ldFlda.Target; } return value as LdLoca; } return null; } /// /// Use the union-find structure to merge /// /// /// Instructions in a group are stores to the same variable that must stay together (cannot be split). /// class GroupStores : ReachingDefinitionsVisitor { readonly UnionFind unionFind = new UnionFind(); /// /// For each uninitialized variable, one representative instruction that /// potentially observes the unintialized value of the variable. /// Used to merge together all such loads of the same uninitialized value. /// readonly Dictionary uninitVariableUsage = new Dictionary(); public GroupStores(ILFunction scope, CancellationToken cancellationToken) : base(scope, IsCandidateVariable, cancellationToken) { } protected internal override void VisitLdLoc(LdLoc inst) { base.VisitLdLoc(inst); HandleLoad(inst); var refLocalAddressLoad = GetAddressLoadForRefLocalUse(inst); if (refLocalAddressLoad != null) { // SupportedRefLocal: act as if we copy-propagated the ldloca // to the point of use: HandleLoad(refLocalAddressLoad); } } protected internal override void VisitLdLoca(LdLoca inst) { base.VisitLdLoca(inst); HandleLoad(inst); } void HandleLoad(IInstructionWithVariableOperand inst) { if (IsAnalyzedVariable(inst.Variable)) { if (IsPotentiallyUninitialized(state, inst.Variable)) { // merge all uninit loads together: if (uninitVariableUsage.TryGetValue(inst.Variable, out var uninitLoad)) { unionFind.Merge(inst, uninitLoad); } else { uninitVariableUsage.Add(inst.Variable, inst); } } foreach (var store in GetStores(state, inst.Variable)) { unionFind.Merge(inst, (IInstructionWithVariableOperand)store); } } } readonly Dictionary newVariables = new Dictionary(); /// /// Gets the new variable for a LdLoc, StLoc or TryCatchHandler instruction. /// internal ILVariable GetNewVariable(IInstructionWithVariableOperand inst) { var representative = unionFind.Find(inst); ILVariable v; if (!newVariables.TryGetValue(representative, out v)) { v = new ILVariable(inst.Variable.Kind, inst.Variable.Type, inst.Variable.StackType, inst.Variable.Index); v.Name = inst.Variable.Name; v.HasGeneratedName = inst.Variable.HasGeneratedName; v.StateMachineField = inst.Variable.StateMachineField; v.HasInitialValue = false; // we'll set HasInitialValue when we encounter an uninit load newVariables.Add(representative, v); inst.Variable.Function.Variables.Add(v); } if (inst.Variable.HasInitialValue && uninitVariableUsage.TryGetValue(inst.Variable, out var uninitLoad) && uninitLoad == inst) { v.HasInitialValue = true; } return v; } } } }