Skip to content

Commit 9eb00e4

Browse files
authored
ILProcessor should also update custom debug info (#867)
* ILProcessor should also update custom debug info (#34) * ILProcessor should also update custom debug info When eidting IL with ILProcessor various pieces of debug information have references to the origin IL instructions. These references can be either resolved (point to Instruction instance), in which case the editting mostly works, or unresolved (store IL offset only) in which case they need to be resolved before the editting can occur (after the edit the original IL offsets are invalid and unresolvable). This is effectively a continuation of #687 which implemented this for local scopes. This change extends this to async method stepping info and state machine scopes. The change refactors the code to make it easier to reuse the same logic between the various debug infos being processed. Updated the existing tests from #687 to include async and state machine debug info (completely made up) and validate that it gets updated correctly. * PR Feedback Renamed some parameters/locals to better match the existing code style. * PR Feedback * Fix test on Linux Native PDB is not supported on Linux and the test infra falls back to portable PDB automatically. Since the two PDB implementations read the custom debug info from a different place the test constructing the input needs to adapt to this difference as well.
1 parent 7d36386 commit 9eb00e4

File tree

2 files changed

+263
-93
lines changed

2 files changed

+263
-93
lines changed

Mono.Cecil.Cil/MethodBody.cs

Lines changed: 146 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ protected override void OnInsert (Instruction item, int index)
258258
item.next = current;
259259
}
260260

261-
UpdateLocalScopes (null, null);
261+
UpdateDebugInformation (null, null);
262262
}
263263

264264
protected override void OnSet (Instruction item, int index)
@@ -271,7 +271,7 @@ protected override void OnSet (Instruction item, int index)
271271
current.previous = null;
272272
current.next = null;
273273

274-
UpdateLocalScopes (item, current);
274+
UpdateDebugInformation (item, current);
275275
}
276276

277277
protected override void OnRemove (Instruction item, int index)
@@ -285,7 +285,7 @@ protected override void OnRemove (Instruction item, int index)
285285
next.previous = item.previous;
286286

287287
RemoveSequencePoint (item);
288-
UpdateLocalScopes (item, next ?? previous);
288+
UpdateDebugInformation (item, next ?? previous);
289289

290290
item.previous = null;
291291
item.next = null;
@@ -306,126 +306,189 @@ void RemoveSequencePoint (Instruction instruction)
306306
}
307307
}
308308

309-
void UpdateLocalScopes (Instruction removedInstruction, Instruction existingInstruction)
309+
void UpdateDebugInformation (Instruction removedInstruction, Instruction existingInstruction)
310310
{
311-
var debug_info = method.debug_info;
312-
if (debug_info == null)
313-
return;
314-
315-
// Local scopes store start/end pair of "instruction offsets". Instruction offset can be either resolved, in which case it
311+
// Various bits of debug information store instruction offsets (as "pointers" to the IL)
312+
// Instruction offset can be either resolved, in which case it
316313
// has a reference to Instruction, or unresolved in which case it stores numerical offset (instruction offset in the body).
317-
// Typically local scopes loaded from PE/PDB files will be resolved, but it's not a requirement.
314+
// Depending on where the InstructionOffset comes from (loaded from PE/PDB or constructed) it can be in either state.
318315
// Each instruction has its own offset, which is populated on load, but never updated (this would be pretty expensive to do).
319316
// Instructions created during the editting will typically have offset 0 (so incorrect).
320-
// Local scopes created during editing will also likely be resolved (so no numerical offsets).
321-
// So while local scopes which are unresolved are relatively rare if they appear, manipulating them based
322-
// on the offsets allone is pretty hard (since we can't rely on correct offsets of instructions).
323-
// On the other hand resolved local scopes are easy to maintain, since they point to instructions and thus inserting
317+
// Manipulating unresolved InstructionOffsets is pretty hard (since we can't rely on correct offsets of instructions).
318+
// On the other hand resolved InstructionOffsets are easy to maintain, since they point to instructions and thus inserting
324319
// instructions is basically a no-op and removing instructions is as easy as changing the pointer.
325320
// For this reason the algorithm here is:
326321
// - First make sure that all instruction offsets are resolved - if not - resolve them
327-
// - First time this will be relatively expensinve as it will walk the entire method body to convert offsets to instruction pointers
328-
// Almost all local scopes are stored in the "right" order (sequentially per start offsets), so the code uses a simple one-item
329-
// cache instruction<->offset to avoid walking instructions multiple times (that would only happen for scopes which are out of order).
330-
// - Subsequent calls should be cheap as it will only walk all local scopes without doing anything
331-
// - If there was an edit on local scope which makes some of them unresolved, the cost is proportional
322+
// - First time this will be relatively expensive as it will walk the entire method body to convert offsets to instruction pointers
323+
// Within the same debug info, IL offsets are typically stored in the "right" order (sequentially per start offsets),
324+
// so the code uses a simple one-item cache instruction<->offset to avoid walking instructions multiple times
325+
// (that would only happen for scopes which are out of order).
326+
// - Subsequent calls should be cheap as it will only walk all local scopes without doing anything (as it checks that they're resolved)
327+
// - If there was an edit which adds some unresolved, the cost is proportional (the code will only resolve those)
332328
// - Then update as necessary by manipulaitng instruction references alone
333329

334-
InstructionOffsetCache cache = new InstructionOffsetCache () {
335-
Offset = 0,
336-
Index = 0,
337-
Instruction = items [0]
338-
};
330+
InstructionOffsetResolver resolver = new InstructionOffsetResolver (items, removedInstruction, existingInstruction);
331+
332+
if (method.debug_info != null)
333+
UpdateLocalScope (method.debug_info.Scope, ref resolver);
334+
335+
var custom_debug_infos = method.custom_infos ?? method.debug_info?.custom_infos;
336+
if (custom_debug_infos != null) {
337+
foreach (var custom_debug_info in custom_debug_infos) {
338+
switch (custom_debug_info) {
339+
case StateMachineScopeDebugInformation state_machine_scope:
340+
UpdateStateMachineScope (state_machine_scope, ref resolver);
341+
break;
339342

340-
UpdateLocalScope (debug_info.Scope, removedInstruction, existingInstruction, ref cache);
343+
case AsyncMethodBodyDebugInformation async_method_body:
344+
UpdateAsyncMethodBody (async_method_body, ref resolver);
345+
break;
346+
347+
default:
348+
// No need to update the other debug info as they don't store instruction references
349+
break;
350+
}
351+
}
352+
}
341353
}
342354

343-
void UpdateLocalScope (ScopeDebugInformation scope, Instruction removedInstruction, Instruction existingInstruction, ref InstructionOffsetCache cache)
355+
void UpdateLocalScope (ScopeDebugInformation scope, ref InstructionOffsetResolver resolver)
344356
{
345357
if (scope == null)
346358
return;
347359

348-
if (!scope.Start.IsResolved)
349-
scope.Start = ResolveInstructionOffset (scope.Start, ref cache);
350-
351-
if (!scope.Start.IsEndOfMethod && scope.Start.ResolvedInstruction == removedInstruction)
352-
scope.Start = new InstructionOffset (existingInstruction);
360+
scope.Start = resolver.Resolve (scope.Start);
353361

354362
if (scope.HasScopes) {
355363
foreach (var subScope in scope.Scopes)
356-
UpdateLocalScope (subScope, removedInstruction, existingInstruction, ref cache);
364+
UpdateLocalScope (subScope, ref resolver);
357365
}
358366

359-
if (!scope.End.IsResolved)
360-
scope.End = ResolveInstructionOffset (scope.End, ref cache);
361-
362-
if (!scope.End.IsEndOfMethod && scope.End.ResolvedInstruction == removedInstruction)
363-
scope.End = new InstructionOffset (existingInstruction);
367+
scope.End = resolver.Resolve (scope.End);
364368
}
365369

366-
struct InstructionOffsetCache {
367-
public int Offset;
368-
public int Index;
369-
public Instruction Instruction;
370+
void UpdateStateMachineScope (StateMachineScopeDebugInformation debugInfo, ref InstructionOffsetResolver resolver)
371+
{
372+
resolver.Restart ();
373+
foreach (var scope in debugInfo.Scopes) {
374+
scope.Start = resolver.Resolve (scope.Start);
375+
scope.End = resolver.Resolve (scope.End);
376+
}
370377
}
371378

372-
InstructionOffset ResolveInstructionOffset(InstructionOffset inputOffset, ref InstructionOffsetCache cache)
379+
void UpdateAsyncMethodBody (AsyncMethodBodyDebugInformation debugInfo, ref InstructionOffsetResolver resolver)
373380
{
374-
if (inputOffset.IsResolved)
375-
return inputOffset;
381+
if (!debugInfo.CatchHandler.IsResolved) {
382+
resolver.Restart ();
383+
debugInfo.CatchHandler = resolver.Resolve (debugInfo.CatchHandler);
384+
}
376385

377-
int offset = inputOffset.Offset;
386+
resolver.Restart ();
387+
for (int i = 0; i < debugInfo.Yields.Count; i++) {
388+
debugInfo.Yields [i] = resolver.Resolve (debugInfo.Yields [i]);
389+
}
378390

379-
if (cache.Offset == offset)
380-
return new InstructionOffset (cache.Instruction);
391+
resolver.Restart ();
392+
for (int i = 0; i < debugInfo.Resumes.Count; i++) {
393+
debugInfo.Resumes [i] = resolver.Resolve (debugInfo.Resumes [i]);
394+
}
395+
}
381396

382-
if (cache.Offset > offset) {
383-
// This should be rare - we're resolving offset pointing to a place before the current cache position
384-
// resolve by walking the instructions from start and don't cache the result.
385-
int size = 0;
386-
for (int i = 0; i < items.Length; i++) {
387-
// The array can be larger than the actual size, in which case its padded with nulls at the end
388-
// so when we reach null, treat it as an end of the IL.
389-
if (items [i] == null)
390-
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
397+
struct InstructionOffsetResolver {
398+
readonly Instruction [] items;
399+
readonly Instruction removed_instruction;
400+
readonly Instruction existing_instruction;
391401

392-
if (size == offset)
393-
return new InstructionOffset (items [i]);
402+
int cache_offset;
403+
int cache_index;
404+
Instruction cache_instruction;
394405

395-
if (size > offset)
396-
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
406+
public int LastOffset { get => cache_offset; }
397407

398-
size += items [i].GetSize ();
399-
}
408+
public InstructionOffsetResolver (Instruction[] instructions, Instruction removedInstruction, Instruction existingInstruction)
409+
{
410+
items = instructions;
411+
removed_instruction = removedInstruction;
412+
existing_instruction = existingInstruction;
413+
cache_offset = 0;
414+
cache_index = 0;
415+
cache_instruction = items [0];
416+
}
400417

401-
// Offset is larger than the size of the body - so it points after the end
402-
return new InstructionOffset ();
403-
} else {
404-
// The offset points after the current cache position - so continue counting and update the cache
405-
int size = cache.Offset;
406-
for (int i = cache.Index; i < items.Length; i++) {
407-
cache.Index = i;
408-
cache.Offset = size;
418+
public void Restart ()
419+
{
420+
cache_offset = 0;
421+
cache_index = 0;
422+
cache_instruction = items [0];
423+
}
409424

410-
var item = items [i];
425+
public InstructionOffset Resolve (InstructionOffset inputOffset)
426+
{
427+
var result = ResolveInstructionOffset (inputOffset);
428+
if (!result.IsEndOfMethod && result.ResolvedInstruction == removed_instruction)
429+
result = new InstructionOffset (existing_instruction);
411430

412-
// Allow for trailing null values in the case of
413-
// instructions.Size < instructions.Capacity
414-
if (item == null)
415-
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
431+
return result;
432+
}
416433

417-
cache.Instruction = item;
434+
InstructionOffset ResolveInstructionOffset (InstructionOffset inputOffset)
435+
{
436+
if (inputOffset.IsResolved)
437+
return inputOffset;
418438

419-
if (cache.Offset == offset)
420-
return new InstructionOffset (cache.Instruction);
439+
int offset = inputOffset.Offset;
421440

422-
if (cache.Offset > offset)
423-
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
441+
if (cache_offset == offset)
442+
return new InstructionOffset (cache_instruction);
424443

425-
size += item.GetSize ();
426-
}
444+
if (cache_offset > offset) {
445+
// This should be rare - we're resolving offset pointing to a place before the current cache position
446+
// resolve by walking the instructions from start and don't cache the result.
447+
int size = 0;
448+
for (int i = 0; i < items.Length; i++) {
449+
// The array can be larger than the actual size, in which case its padded with nulls at the end
450+
// so when we reach null, treat it as an end of the IL.
451+
if (items [i] == null)
452+
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
453+
454+
if (size == offset)
455+
return new InstructionOffset (items [i]);
456+
457+
if (size > offset)
458+
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
459+
460+
size += items [i].GetSize ();
461+
}
462+
463+
// Offset is larger than the size of the body - so it points after the end
464+
return new InstructionOffset ();
465+
} else {
466+
// The offset points after the current cache position - so continue counting and update the cache
467+
int size = cache_offset;
468+
for (int i = cache_index; i < items.Length; i++) {
469+
cache_index = i;
470+
cache_offset = size;
427471

428-
return new InstructionOffset ();
472+
var item = items [i];
473+
474+
// Allow for trailing null values in the case of
475+
// instructions.Size < instructions.Capacity
476+
if (item == null)
477+
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
478+
479+
cache_instruction = item;
480+
481+
if (cache_offset == offset)
482+
return new InstructionOffset (cache_instruction);
483+
484+
if (cache_offset > offset)
485+
return new InstructionOffset (i == 0 ? items [0] : items [i - 1]);
486+
487+
size += item.GetSize ();
488+
}
489+
490+
return new InstructionOffset ();
491+
}
429492
}
430493
}
431494
}

0 commit comments

Comments
 (0)