1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics;
8 using System.Reflection.Internal;
9 
10 namespace System.Reflection.Metadata.Ecma335
11 {
12     public sealed class ControlFlowBuilder
13     {
14         // internal for testing:
15         internal readonly struct BranchInfo
16         {
17             internal readonly int ILOffset;
18             internal readonly LabelHandle Label;
19             private readonly byte _opCode;
20 
21             internal ILOpCode OpCode => (ILOpCode)_opCode;
22 
BranchInfoSystem.Reflection.Metadata.Ecma335.ControlFlowBuilder.BranchInfo23             internal BranchInfo(int ilOffset, LabelHandle label, ILOpCode opCode)
24             {
25                 ILOffset = ilOffset;
26                 Label = label;
27                 _opCode = (byte)opCode;
28             }
29 
IsShortBranchDistanceSystem.Reflection.Metadata.Ecma335.ControlFlowBuilder.BranchInfo30             internal bool IsShortBranchDistance(ImmutableArray<int>.Builder labels, out int distance)
31             {
32                 const int shortBranchSize = 2;
33                 const int longBranchSize = 5;
34 
35                 int labelTargetOffset = labels[Label.Id - 1];
36                 if (labelTargetOffset < 0)
37                 {
38                     Throw.InvalidOperation_LabelNotMarked(Label.Id);
39                 }
40 
41                 distance = labelTargetOffset - (ILOffset + shortBranchSize);
42                 if (unchecked((sbyte)distance) == distance)
43                 {
44                     return true;
45                 }
46 
47                 distance = labelTargetOffset - (ILOffset + longBranchSize);
48                 return false;
49             }
50         }
51 
52         internal readonly struct ExceptionHandlerInfo
53         {
54             public readonly ExceptionRegionKind Kind;
55             public readonly LabelHandle TryStart, TryEnd, HandlerStart, HandlerEnd, FilterStart;
56             public readonly EntityHandle CatchType;
57 
ExceptionHandlerInfoSystem.Reflection.Metadata.Ecma335.ControlFlowBuilder.ExceptionHandlerInfo58             public ExceptionHandlerInfo(
59                 ExceptionRegionKind kind,
60                 LabelHandle tryStart,
61                 LabelHandle tryEnd,
62                 LabelHandle handlerStart,
63                 LabelHandle handlerEnd,
64                 LabelHandle filterStart,
65                 EntityHandle catchType)
66             {
67                 Kind = kind;
68                 TryStart = tryStart;
69                 TryEnd = tryEnd;
70                 HandlerStart = handlerStart;
71                 HandlerEnd = handlerEnd;
72                 FilterStart = filterStart;
73                 CatchType = catchType;
74             }
75         }
76 
77         private readonly ImmutableArray<BranchInfo>.Builder _branches;
78         private readonly ImmutableArray<int>.Builder _labels;
79         private ImmutableArray<ExceptionHandlerInfo>.Builder _lazyExceptionHandlers;
80 
ControlFlowBuilder()81         public ControlFlowBuilder()
82         {
83             _branches = ImmutableArray.CreateBuilder<BranchInfo>();
84             _labels = ImmutableArray.CreateBuilder<int>();
85         }
86 
Clear()87         internal void Clear()
88         {
89             _branches.Clear();
90             _labels.Clear();
91             _lazyExceptionHandlers?.Clear();
92         }
93 
AddLabel()94         internal LabelHandle AddLabel()
95         {
96             _labels.Add(-1);
97             return new LabelHandle(_labels.Count);
98         }
99 
AddBranch(int ilOffset, LabelHandle label, ILOpCode opCode)100         internal void AddBranch(int ilOffset, LabelHandle label, ILOpCode opCode)
101         {
102             Debug.Assert(ilOffset >= 0);
103             Debug.Assert(_branches.Count == 0 || ilOffset > _branches.Last().ILOffset);
104             ValidateLabel(label, nameof(label));
105             _branches.Add(new BranchInfo(ilOffset, label, opCode));
106         }
107 
MarkLabel(int ilOffset, LabelHandle label)108         internal void MarkLabel(int ilOffset, LabelHandle label)
109         {
110             Debug.Assert(ilOffset >= 0);
111             ValidateLabel(label, nameof(label));
112             _labels[label.Id - 1] = ilOffset;
113         }
114 
GetLabelOffsetChecked(LabelHandle label)115         private int GetLabelOffsetChecked(LabelHandle label)
116         {
117             int offset = _labels[label.Id - 1];
118             if (offset < 0)
119             {
120                 Throw.InvalidOperation_LabelNotMarked(label.Id);
121             }
122 
123             return offset;
124         }
125 
ValidateLabel(LabelHandle label, string parameterName)126         private void ValidateLabel(LabelHandle label, string parameterName)
127         {
128             if (label.IsNil)
129             {
130                 Throw.ArgumentNull(parameterName);
131             }
132 
133             if (label.Id > _labels.Count)
134             {
135                 Throw.LabelDoesntBelongToBuilder(parameterName);
136             }
137         }
138 
139         /// <summary>
140         /// Adds finally region.
141         /// </summary>
142         /// <param name="tryStart">Label marking the first instruction of the try block.</param>
143         /// <param name="tryEnd">Label marking the instruction immediately following the try block.</param>
144         /// <param name="handlerStart">Label marking the first instruction of the handler.</param>
145         /// <param name="handlerEnd">Label marking the instruction immediately following the handler.</param>
146         /// <returns>Encoder for the next clause.</returns>
147         /// <exception cref="ArgumentException">A label was not defined by an instruction encoder this builder is associated with.</exception>
148         /// <exception cref="ArgumentNullException">A label has default value.</exception>
AddFinallyRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd)149         public void AddFinallyRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd) =>
150             AddExceptionRegion(ExceptionRegionKind.Finally, tryStart, tryEnd, handlerStart, handlerEnd);
151 
152         /// <summary>
153         /// Adds fault region.
154         /// </summary>
155         /// <param name="tryStart">Label marking the first instruction of the try block.</param>
156         /// <param name="tryEnd">Label marking the instruction immediately following the try block.</param>
157         /// <param name="handlerStart">Label marking the first instruction of the handler.</param>
158         /// <param name="handlerEnd">Label marking the instruction immediately following the handler.</param>
159         /// <exception cref="ArgumentException">A label was not defined by an instruction encoder this builder is associated with.</exception>
160         /// <exception cref="ArgumentNullException">A label has default value.</exception>
AddFaultRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd)161         public void AddFaultRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd) =>
162             AddExceptionRegion(ExceptionRegionKind.Fault, tryStart, tryEnd, handlerStart, handlerEnd);
163 
164         /// <summary>
165         /// Adds catch region.
166         /// </summary>
167         /// <param name="tryStart">Label marking the first instruction of the try block.</param>
168         /// <param name="tryEnd">Label marking the instruction immediately following the try block.</param>
169         /// <param name="handlerStart">Label marking the first instruction of the handler.</param>
170         /// <param name="handlerEnd">Label marking the instruction immediately following the handler.</param>
171         /// <param name="catchType">The type of exception to be caught: <see cref="TypeDefinitionHandle"/>, <see cref="TypeReferenceHandle"/> or <see cref="TypeSpecificationHandle"/>.</param>
172         /// <exception cref="ArgumentException">A label was not defined by an instruction encoder this builder is associated with.</exception>
173         /// <exception cref="ArgumentException"><paramref name="catchType"/> is not a valid type handle.</exception>
174         /// <exception cref="ArgumentNullException">A label has default value.</exception>
AddCatchRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd, EntityHandle catchType)175         public void AddCatchRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd, EntityHandle catchType)
176         {
177             if (!ExceptionRegionEncoder.IsValidCatchTypeHandle(catchType))
178             {
179                 Throw.InvalidArgument_Handle(nameof(catchType));
180             }
181 
182             AddExceptionRegion(ExceptionRegionKind.Catch, tryStart, tryEnd, handlerStart, handlerEnd, catchType: catchType);
183         }
184 
185         /// <summary>
186         /// Adds catch region.
187         /// </summary>
188         /// <param name="tryStart">Label marking the first instruction of the try block.</param>
189         /// <param name="tryEnd">Label marking the instruction immediately following the try block.</param>
190         /// <param name="handlerStart">Label marking the first instruction of the handler.</param>
191         /// <param name="handlerEnd">Label marking the instruction immediately following the handler.</param>
192         /// <param name="filterStart">Label marking the first instruction of the filter block.</param>
193         /// <exception cref="ArgumentException">A label was not defined by an instruction encoder this builder is associated with.</exception>
194         /// <exception cref="ArgumentNullException">A label has default value.</exception>
AddFilterRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd, LabelHandle filterStart)195         public void AddFilterRegion(LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd, LabelHandle filterStart)
196         {
197             ValidateLabel(filterStart, nameof(filterStart));
198             AddExceptionRegion(ExceptionRegionKind.Filter, tryStart, tryEnd, handlerStart, handlerEnd, filterStart: filterStart);
199         }
200 
AddExceptionRegion( ExceptionRegionKind kind, LabelHandle tryStart, LabelHandle tryEnd, LabelHandle handlerStart, LabelHandle handlerEnd, LabelHandle filterStart = default(LabelHandle), EntityHandle catchType = default(EntityHandle))201         private void AddExceptionRegion(
202             ExceptionRegionKind kind,
203             LabelHandle tryStart,
204             LabelHandle tryEnd,
205             LabelHandle handlerStart,
206             LabelHandle handlerEnd,
207             LabelHandle filterStart = default(LabelHandle),
208             EntityHandle catchType = default(EntityHandle))
209         {
210             ValidateLabel(tryStart, nameof(tryStart));
211             ValidateLabel(tryEnd, nameof(tryEnd));
212             ValidateLabel(handlerStart, nameof(handlerStart));
213             ValidateLabel(handlerEnd, nameof(handlerEnd));
214 
215             if (_lazyExceptionHandlers == null)
216             {
217                 _lazyExceptionHandlers = ImmutableArray.CreateBuilder<ExceptionHandlerInfo>();
218             }
219 
220             _lazyExceptionHandlers.Add(new ExceptionHandlerInfo(kind, tryStart, tryEnd, handlerStart, handlerEnd, filterStart, catchType));
221         }
222 
223         // internal for testing:
224         internal IEnumerable<BranchInfo> Branches => _branches;
225 
226         // internal for testing:
227         internal IEnumerable<int> Labels => _labels;
228 
229         internal int BranchCount => _branches.Count;
230 
231         internal int ExceptionHandlerCount => _lazyExceptionHandlers?.Count ?? 0;
232 
233         /// <exception cref="InvalidOperationException" />
CopyCodeAndFixupBranches(BlobBuilder srcBuilder, BlobBuilder dstBuilder)234         internal void CopyCodeAndFixupBranches(BlobBuilder srcBuilder, BlobBuilder dstBuilder)
235         {
236             var branch = _branches[0];
237             int branchIndex = 0;
238 
239             // offset within the source builder
240             int srcOffset = 0;
241 
242             // current offset within the current source blob
243             int srcBlobOffset = 0;
244 
245             foreach (Blob srcBlob in srcBuilder.GetBlobs())
246             {
247                 Debug.Assert(
248                     srcBlobOffset == 0 ||
249                     srcBlobOffset == 1 && srcBlob.Buffer[0] == 0xff ||
250                     srcBlobOffset == 4 && srcBlob.Buffer[0] == 0xff && srcBlob.Buffer[1] == 0xff && srcBlob.Buffer[2] == 0xff && srcBlob.Buffer[3] == 0xff);
251 
252                 while (true)
253                 {
254                     // copy bytes preceding the next branch, or till the end of the blob:
255                     int chunkSize = Math.Min(branch.ILOffset - srcOffset, srcBlob.Length - srcBlobOffset);
256                     dstBuilder.WriteBytes(srcBlob.Buffer, srcBlobOffset, chunkSize);
257                     srcOffset += chunkSize;
258                     srcBlobOffset += chunkSize;
259 
260                     // there is no branch left in the blob:
261                     if (srcBlobOffset == srcBlob.Length)
262                     {
263                         srcBlobOffset = 0;
264                         break;
265                     }
266 
267                     Debug.Assert(srcBlob.Buffer[srcBlobOffset] == (byte)branch.OpCode);
268 
269                     int operandSize = branch.OpCode.GetBranchOperandSize();
270                     bool isShortInstruction = operandSize == 1;
271 
272                     // Note: the 4B operand is contiguous since we wrote it via BlobBuilder.WriteInt32()
273                     Debug.Assert(
274                         srcBlobOffset + 1 == srcBlob.Length ||
275                         (isShortInstruction ?
276                            srcBlob.Buffer[srcBlobOffset + 1] == 0xff :
277                            BitConverter.ToUInt32(srcBlob.Buffer, srcBlobOffset + 1) == 0xffffffff));
278 
279                     // write branch opcode:
280                     dstBuilder.WriteByte(srcBlob.Buffer[srcBlobOffset]);
281 
282                     // write branch operand:
283                     int branchDistance;
284                     bool isShortDistance = branch.IsShortBranchDistance(_labels, out branchDistance);
285 
286                     if (isShortInstruction && !isShortDistance)
287                     {
288                         // We could potentially implement algortihm that automatically fixes up the branch instructions as well to accomodate bigger distances,
289                         // however an optimal algorithm would be rather complex (something like: calculate topological ordering of crossing branch instructions
290                         // and then use fixed point to eliminate cycles). If the caller doesn't care about optimal IL size they can use long branches whenever the
291                         // distance is unknown upfront. If they do they probably already implement more sophisticad algorithm for IL layout optimization already.
292                         throw new InvalidOperationException(SR.Format(SR.DistanceBetweenInstructionAndLabelTooBig, branch.OpCode, srcOffset, branchDistance));
293                     }
294 
295                     if (isShortInstruction)
296                     {
297                         dstBuilder.WriteSByte((sbyte)branchDistance);
298                     }
299                     else
300                     {
301                         dstBuilder.WriteInt32(branchDistance);
302                     }
303 
304                     srcOffset += sizeof(byte) + operandSize;
305 
306                     // next branch:
307                     branchIndex++;
308                     if (branchIndex == _branches.Count)
309                     {
310                         branch = new BranchInfo(int.MaxValue, default(LabelHandle), 0);
311                     }
312                     else
313                     {
314                         branch = _branches[branchIndex];
315                     }
316 
317                     // the branch starts at the very end and its operand is in the next blob:
318                     if (srcBlobOffset == srcBlob.Length - 1)
319                     {
320                         srcBlobOffset = operandSize;
321                         break;
322                     }
323 
324                     // skip fake branch instruction:
325                     srcBlobOffset += sizeof(byte) + operandSize;
326                 }
327             }
328         }
329 
SerializeExceptionTable(BlobBuilder builder)330         internal void SerializeExceptionTable(BlobBuilder builder)
331         {
332             if (_lazyExceptionHandlers == null || _lazyExceptionHandlers.Count == 0)
333             {
334                 return;
335             }
336 
337             var regionEncoder = ExceptionRegionEncoder.SerializeTableHeader(builder, _lazyExceptionHandlers.Count, HasSmallExceptionRegions());
338 
339             foreach (var handler in _lazyExceptionHandlers)
340             {
341                 // Note that labels have been validated when added to the handler list,
342                 // they might not have been marked though.
343 
344                 int tryStart = GetLabelOffsetChecked(handler.TryStart);
345                 int tryEnd = GetLabelOffsetChecked(handler.TryEnd);
346                 int handlerStart = GetLabelOffsetChecked(handler.HandlerStart);
347                 int handlerEnd = GetLabelOffsetChecked(handler.HandlerEnd);
348 
349                 if (tryStart > tryEnd)
350                 {
351                     Throw.InvalidOperation(SR.Format(SR.InvalidExceptionRegionBounds, tryStart, tryEnd));
352                 }
353 
354                 if (handlerStart > handlerEnd)
355                 {
356                     Throw.InvalidOperation(SR.Format(SR.InvalidExceptionRegionBounds, handlerStart, handlerEnd));
357                 }
358 
359                 int catchTokenOrOffset;
360                 switch (handler.Kind)
361                 {
362                     case ExceptionRegionKind.Catch:
363                         catchTokenOrOffset = MetadataTokens.GetToken(handler.CatchType);
364                         break;
365 
366                     case ExceptionRegionKind.Filter:
367                         catchTokenOrOffset = GetLabelOffsetChecked(handler.FilterStart);
368                         break;
369 
370                     default:
371                         catchTokenOrOffset = 0;
372                         break;
373                 }
374 
375                 regionEncoder.AddUnchecked(
376                     handler.Kind,
377                     tryStart,
378                     tryEnd - tryStart,
379                     handlerStart,
380                     handlerEnd - handlerStart,
381                     catchTokenOrOffset);
382             }
383         }
384 
HasSmallExceptionRegions()385         private bool HasSmallExceptionRegions()
386         {
387             Debug.Assert(_lazyExceptionHandlers != null);
388 
389             if (!ExceptionRegionEncoder.IsSmallRegionCount(_lazyExceptionHandlers.Count))
390             {
391                 return false;
392             }
393 
394             foreach (var handler in _lazyExceptionHandlers)
395             {
396                 if (!ExceptionRegionEncoder.IsSmallExceptionRegionFromBounds(GetLabelOffsetChecked(handler.TryStart), GetLabelOffsetChecked(handler.TryEnd)) ||
397                     !ExceptionRegionEncoder.IsSmallExceptionRegionFromBounds(GetLabelOffsetChecked(handler.HandlerStart), GetLabelOffsetChecked(handler.HandlerEnd)))
398                 {
399                     return false;
400                 }
401             }
402 
403             return true;
404         }
405     }
406 }
407