#4122·ILSpy

Sequence points can land in the middle of an IL instruction

Author: siegfriedpammerCreated Sep 8, 2026Updated Sep 8, 2026
LabelsBugPDBGen

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

csharp
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.dll
Minimal.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=32

Visible 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.