Sequence points can land in the middle of an IL instruction
Summary
Sequence points in a generated PDB can carry an IL offset that is not the start of an instruction. A consumer that resolves such an offset to an instruction gets nothing back: Mono.Cecil's GetInstruction returns null, and a debugger asked to bind a breakpoint there has no instruction to bind to.
Only visible in assemblies compiled in Release. In a Debug build the first instruction of every method is a one-byte nop, which happens to make the offending offset land on a boundary.
Reproduction
using System;
namespace ProbeNs
{
public struct Holder
{
public int Value;
}
public class Minimal
{
public static int UseDefault(int input)
{
Holder holder = default(Holder);
holder.Value = input;
return holder.Value;
}
public static Func<int, int> MakeDelegate(int offset)
{
return (int n) => n + offset;
}
}
}dotnet build -c Release
ilspycmd -genpdb Probe.dllMinimal.UseDefault: sequence point at IL_0001 (hidden) is inside IL_0000: ldloca.s (2 bytes)
Minimal.MakeDelegate: sequence point at IL_0001 (hidden) is inside IL_0000: newobj (5 bytes)The same source built in Debug produces no such point.
The full point list for one of them shows the shape - a one-byte first point, and the hidden filler that follows it inherits its end offset:
off=0x0 visible line=30
off=0x1 hidden <- inside the 5-byte instruction at IL_0000
off=0x14 visible line=31
off=0x23 hidden
off=0x29 visible line=32Visible sequence points are affected too, not only hidden ones. In Microsoft.Extensions.Http 10.0.11, <<SendCoreAsync>g__Core|4_0>d.MoveNext gets a visible point for line 62 at IL_000b, inside the 5-byte ldfld at IL_0008.
Scale over shipping assemblies: 156 such points in Newtonsoft.Json 13.0.4, 21 in AutoMapper 16.2.0, 4 in Microsoft.Extensions.Http 10.0.11.
Context
In SequencePointBuilder.GetSequencePoints (ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs), the loop that snaps offsets to function.SequencePointCandidates adjusts EndOffset of the current point and Offset of the next one only when the neighbouring-point conditions hold; the gap-filling insert at the end of the loop body then takes currSequencePoint.EndOffset verbatim as the new point's Offset. SequencePointCandidates itself only ever contains inst.StartILOffset values (ILReader.cs line 560), so the candidate list is not the source of the bad offsets.
Posted by an AI agent (Claude) on Siegfried's behalf.
Source: icsharpcode/ILSpy