You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
12 KiB

  1. // Copyright (c) 2012 AlphaSierraPapa for the SharpDevelop Team
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4. // software and associated documentation files (the "Software"), to deal in the Software
  5. // without restriction, including without limitation the rights to use, copy, modify, merge,
  6. // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7. // to whom the Software is furnished to do so, subject to the following conditions:
  8. //
  9. // The above copyright notice and this permission notice shall be included in all copies or
  10. // substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  13. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  14. // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  15. // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  16. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. // DEALINGS IN THE SOFTWARE.
  18. #nullable enable
  19. using System;
  20. using System.Collections.Generic;
  21. using System.Diagnostics;
  22. using System.Linq;
  23. using System.Threading;
  24. using ICSharpCode.Decompiler.TypeSystem;
  25. using ICSharpCode.Decompiler.Util;
  26. namespace ICSharpCode.Decompiler.IL.ControlFlow
  27. {
  28. enum StateRangeAnalysisMode
  29. {
  30. IteratorMoveNext,
  31. IteratorDispose,
  32. AsyncMoveNext,
  33. AwaitInFinally
  34. }
  35. /// <summary>
  36. /// Symbolically executes code to determine which blocks are reachable for which values
  37. /// of the 'state' field.
  38. /// </summary>
  39. /// <remarks>
  40. /// Assumption: there are no loops/backward jumps
  41. /// We 'run' the code, with "state" being a symbolic variable
  42. /// so it can form expressions like "state + x" (when there's a sub instruction)
  43. ///
  44. /// For each block, we maintain the set of values for state for which the block is reachable.
  45. /// This is (int.MinValue, int.MaxValue) for the first instruction.
  46. /// These ranges are propagated depending on the conditional jumps performed by the code.
  47. /// </remarks>
  48. class StateRangeAnalysis
  49. {
  50. public CancellationToken CancellationToken;
  51. readonly StateRangeAnalysisMode mode;
  52. readonly IField? stateField;
  53. readonly bool legacyVisualBasic;
  54. readonly SymbolicEvaluationContext evalContext;
  55. readonly Dictionary<Block, LongSet> ranges = new Dictionary<Block, LongSet>();
  56. readonly Dictionary<BlockContainer, LongSet>? rangesForLeave; // used only for AwaitInFinally
  57. readonly internal Dictionary<IMethod, LongSet>? finallyMethodToStateRange; // used only for IteratorDispose
  58. internal ILVariable? doFinallyBodies;
  59. internal ILVariable? skipFinallyBodies;
  60. public StateRangeAnalysis(StateRangeAnalysisMode mode, IField? stateField, ILVariable? cachedStateVar = null, bool legacyVisualBasic = false)
  61. {
  62. this.mode = mode;
  63. this.stateField = stateField;
  64. this.legacyVisualBasic = legacyVisualBasic;
  65. if (mode == StateRangeAnalysisMode.IteratorDispose)
  66. {
  67. finallyMethodToStateRange = new Dictionary<IMethod, LongSet>();
  68. }
  69. if (mode == StateRangeAnalysisMode.AwaitInFinally)
  70. {
  71. rangesForLeave = new Dictionary<BlockContainer, LongSet>();
  72. }
  73. evalContext = new SymbolicEvaluationContext(stateField, legacyVisualBasic);
  74. if (cachedStateVar != null)
  75. evalContext.AddStateVariable(cachedStateVar);
  76. }
  77. public IEnumerable<ILVariable> CachedStateVars { get => evalContext.StateVariables; }
  78. /// <summary>
  79. /// Creates a new StateRangeAnalysis with the same settings, including any cached state vars
  80. /// discovered by this analysis.
  81. /// However, the new analysis has a fresh set of result ranges.
  82. /// </summary>
  83. internal StateRangeAnalysis CreateNestedAnalysis()
  84. {
  85. var sra = new StateRangeAnalysis(mode, stateField, legacyVisualBasic: legacyVisualBasic);
  86. sra.doFinallyBodies = this.doFinallyBodies;
  87. sra.skipFinallyBodies = this.skipFinallyBodies;
  88. foreach (var v in this.evalContext.StateVariables)
  89. {
  90. sra.evalContext.AddStateVariable(v);
  91. }
  92. return sra;
  93. }
  94. /// <summary>
  95. /// Assign state ranges for all blocks within 'inst'.
  96. /// </summary>
  97. /// <returns>
  98. /// The set of states for which the exit point of the instruction is reached.
  99. /// This must be a subset of the input set.
  100. ///
  101. /// Returns an empty set for unsupported instructions.
  102. /// </returns>
  103. public LongSet AssignStateRanges(ILInstruction inst, LongSet stateRange)
  104. {
  105. CancellationToken.ThrowIfCancellationRequested();
  106. switch (inst)
  107. {
  108. case BlockContainer blockContainer:
  109. AddStateRange(blockContainer.EntryPoint, stateRange);
  110. foreach (var block in blockContainer.Blocks)
  111. {
  112. // We assume that there are no jumps to blocks already processed.
  113. // TODO: is SortBlocks() guaranteeing this, even if the user code has loops?
  114. if (ranges.TryGetValue(block, out stateRange))
  115. {
  116. AssignStateRanges(block, stateRange);
  117. }
  118. }
  119. // Since we don't track 'leave' edges, we can only conservatively
  120. // return LongSet.Empty.
  121. return LongSet.Empty;
  122. case Block block:
  123. foreach (var instInBlock in block.Instructions)
  124. {
  125. if (stateRange.IsEmpty)
  126. break;
  127. var oldStateRange = stateRange;
  128. stateRange = AssignStateRanges(instInBlock, stateRange);
  129. // End-point can only be reachable in a subset of the states where the start-point is reachable.
  130. Debug.Assert(stateRange.IsSubsetOf(oldStateRange));
  131. // If the end-point is unreachable, it must be reachable in no states.
  132. Debug.Assert(stateRange.IsEmpty || !instInBlock.HasFlag(InstructionFlags.EndPointUnreachable));
  133. }
  134. return stateRange;
  135. case TryFinally tryFinally when mode == StateRangeAnalysisMode.IteratorDispose:
  136. var afterTry = AssignStateRanges(tryFinally.TryBlock, stateRange);
  137. // really finally should start with 'stateRange.UnionWith(afterTry)', but that's
  138. // equal to 'stateRange'.
  139. Debug.Assert(afterTry.IsSubsetOf(stateRange));
  140. var afterFinally = AssignStateRanges(tryFinally.FinallyBlock, stateRange);
  141. return afterTry.IntersectWith(afterFinally);
  142. case SwitchInstruction switchInst:
  143. SymbolicValue val = evalContext.Eval(switchInst.Value);
  144. if (val.Type != SymbolicValueType.State)
  145. goto default;
  146. List<LongInterval> exitIntervals = new List<LongInterval>();
  147. foreach (var section in switchInst.Sections)
  148. {
  149. // switch (state + Constant)
  150. // matches 'case VALUE:'
  151. // iff (state + Constant == value)
  152. // iff (state == value - Constant)
  153. var effectiveLabels = section.Labels.AddOffset(unchecked(-val.Constant));
  154. var result = AssignStateRanges(section.Body, stateRange.IntersectWith(effectiveLabels));
  155. exitIntervals.AddRange(result.Intervals);
  156. }
  157. // exitIntervals = union of exits of all sections
  158. return new LongSet(exitIntervals);
  159. case IfInstruction ifInst:
  160. val = evalContext.Eval(ifInst.Condition).AsBool();
  161. if (val.Type != SymbolicValueType.StateInSet)
  162. {
  163. goto default;
  164. }
  165. LongSet trueRanges = val.ValueSet;
  166. var afterTrue = AssignStateRanges(ifInst.TrueInst, stateRange.IntersectWith(trueRanges));
  167. var afterFalse = AssignStateRanges(ifInst.FalseInst, stateRange.ExceptWith(trueRanges));
  168. return afterTrue.UnionWith(afterFalse);
  169. case Branch br:
  170. AddStateRange(br.TargetBlock, stateRange);
  171. return LongSet.Empty;
  172. case Leave leave when mode == StateRangeAnalysisMode.AwaitInFinally:
  173. AddStateRangeForLeave(leave.TargetContainer, stateRange);
  174. return LongSet.Empty;
  175. case Nop nop:
  176. return stateRange;
  177. case StLoc stloc when stloc.Variable == doFinallyBodies || stloc.Variable == skipFinallyBodies:
  178. // pre-roslyn async/await uses a generated 'bool doFinallyBodies';
  179. // do not treat this as user code.
  180. // Mono also does this for yield-return.
  181. return stateRange;
  182. case StLoc stloc:
  183. val = evalContext.Eval(stloc.Value);
  184. if (val.Type == SymbolicValueType.State && val.Constant == 0)
  185. {
  186. evalContext.AddStateVariable(stloc.Variable);
  187. return stateRange;
  188. }
  189. else
  190. {
  191. goto default; // user code
  192. }
  193. case Call call when mode == StateRangeAnalysisMode.IteratorDispose:
  194. // Call to finally method.
  195. // Usually these are in finally blocks, but sometimes (e.g. foreach over array),
  196. // the C# compiler puts the call to a finally method outside the try-finally block.
  197. finallyMethodToStateRange!.Add((IMethod)call.Method.MemberDefinition, stateRange);
  198. return LongSet.Empty; // return Empty since we executed user code (the finally method)
  199. case StObj stobj when mode == StateRangeAnalysisMode.IteratorMoveNext:
  200. {
  201. if (stobj.MatchStFld(out var target, out var field, out var value)
  202. && target.MatchLdThis() && field.MemberDefinition == stateField && value.MatchLdcI4(-1))
  203. {
  204. // Mono resets the state field during MoveNext();
  205. // don't consider this user code.
  206. return stateRange;
  207. }
  208. else
  209. {
  210. goto default;
  211. }
  212. }
  213. case StObj stobj when mode == StateRangeAnalysisMode.IteratorDispose:
  214. {
  215. if (stobj.MatchStFld(out var target, out var field, out var value) && target.MatchLdThis())
  216. {
  217. if (field.MemberDefinition == stateField && value.MatchLdcI4(-2))
  218. {
  219. // Roslyn 4.13 sets the state field in Dispose() to mark the iterator as disposed,
  220. // don't consider this user code.
  221. return stateRange;
  222. }
  223. else if (value.MatchDefaultOrNullOrZero())
  224. {
  225. // Roslyn 4.13 clears any local hoisted local variables in Dispose(),
  226. // don't consider this user code.
  227. return stateRange;
  228. }
  229. else
  230. {
  231. goto default;
  232. }
  233. }
  234. else
  235. {
  236. goto default;
  237. }
  238. }
  239. default:
  240. // User code - abort analysis
  241. if (mode == StateRangeAnalysisMode.IteratorDispose && !(inst is Leave l && l.IsLeavingFunction))
  242. {
  243. throw new SymbolicAnalysisFailedException("Unexpected instruction in Iterator.Dispose()");
  244. }
  245. return LongSet.Empty;
  246. }
  247. }
  248. private void AddStateRange(Block block, LongSet stateRange)
  249. {
  250. if (ranges.TryGetValue(block, out var existingRange))
  251. ranges[block] = stateRange.UnionWith(existingRange);
  252. else
  253. ranges.Add(block, stateRange);
  254. }
  255. private void AddStateRangeForLeave(BlockContainer target, LongSet stateRange)
  256. {
  257. if (rangesForLeave!.TryGetValue(target, out var existingRange))
  258. rangesForLeave[target] = stateRange.UnionWith(existingRange);
  259. else
  260. rangesForLeave.Add(target, stateRange);
  261. }
  262. /// <summary>
  263. /// Gets a mapping from states to blocks.
  264. ///
  265. /// Within the given container (which should be the container that was analyzed),
  266. /// the mapping prefers the last block.
  267. /// Blocks outside of the given container are preferred over blocks within the container.
  268. /// </summary>
  269. public LongDict<Block> GetBlockStateSetMapping(BlockContainer container)
  270. {
  271. return LongDict.Create(GetMapping());
  272. IEnumerable<(LongSet, Block)> GetMapping()
  273. {
  274. // First, consider container exits:
  275. foreach (var (block, states) in ranges)
  276. {
  277. if (block.Parent != container)
  278. yield return (states, block);
  279. }
  280. // Then blocks within the container:
  281. foreach (var block in container.Blocks.Reverse())
  282. {
  283. if (ranges.TryGetValue(block, out var states))
  284. {
  285. yield return (states, block);
  286. }
  287. }
  288. }
  289. }
  290. public LongDict<BlockContainer> GetBlockStateSetMappingForLeave()
  291. {
  292. Debug.Assert(mode == StateRangeAnalysisMode.AwaitInFinally);
  293. return LongDict.Create(rangesForLeave!.Select(kv => (kv.Value, kv.Key)));
  294. }
  295. }
  296. }