Say hello to FeeFee

At work I get to do some really interesting things with C# and .Net. One of the things I’ve done lately is some work on calculating extra data alongside generated code so that it can be stepped easily inside Visual Studio. It turns out that this all revolves around the idea of a sequence point.

Say we have the following console application, and we start it by stepping into it.

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(1);
            Console.WriteLine(2);
            Console.WriteLine(3);
        }

        // Foo bar
    }
}

Inside Visual Studio, we can hit F10 which stops on the opening { of the Main method. Flipping into the disassembly view, we can see that the debugger has calculated a mapping between the source code and the x86 instructions.

00000024  nop             
            Console.WriteLine(1);
00000025  mov         ecx,1
0000002a  call        FFAC292C
0000002f  nop             
            Console.WriteLine(2);
00000030  mov         ecx,2
00000035  call        FFAC292C
0000003a  nop             
            Console.WriteLine(3);
0000003b  mov         ecx,3
00000040  call        FFAC292C
00000045  nop             
        }
00000046  nop             
00000047  lea         esp,[ebp-0Ch]
0000004a  pop         ebx 
0000004b  pop         esi 

How does it do this, you might ask. If we ildasm the resulting assembly, asking for line information, we get a main method that looks like this.

D:temp4SequencePointDemoSequencePointDemobinDebug>"c:ProgramsMicrosoft Visual Studio 8SDKv2.0Binildasm.exe" /LINENUM /OUT=program.il SequencePointDemo.exe

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  8
  .language ‘{3F5162F8-07C6-11D3-9053-00C04FA302A1}’, ‘{994B45C4-E6E9-11D2-903F-00C04FA302A1}’, ‘{5A869D0B-6611-11D3-BD2A-0000F80849BD}’
  .line 10,10 : 9,10 ‘D:\temp4\SequencePointDemo\SequencePointDemo\Program.cs’
  IL_0000:  nop
  .line 11,11 : 13,34 ”
  IL_0001:  ldc.i4.1
  IL_0002:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0007:  nop
  .line 12,12 : 13,34 ”
  IL_0008:  ldc.i4.2
  IL_0009:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_000e:  nop
  .line 13,13 : 13,34 ”
  IL_000f:  ldc.i4.3
  IL_0010:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0015:  nop
  .line 14,14 : 9,10 ”
  IL_0016:  ret
} // end of method Program::Main

The .line directives are the key. They provide a pair of start and end line numbers, followed by a pair of start and end character positions on those lines, optionally followed by a file name in which those lines reside.

We can demonstrate this by changing the first .line directive after the nop to

IL_0000:  nop
.line 16,16 : 13,34 ”
IL_0001:  ldc.i4.1
IL_0002:  call       void [mscorlib]System.Console::WriteLine(int32)

If we then generate a new exe

D:temp4SequencePointDemoSequencePointDemobinDebug>c:WINDOWSMicrosoft.NETFrameworkv2.0.50727ilasm.exe /debug program.il

and step into that (by setting it as the start executable in Visual Studio), we now get the mapping

        static void Main(string[] args)
        {
00000000  push        ebp 
00000001  mov         ebp,esp
00000003  push        eax 
00000004  mov         dword ptr [ebp-4],ecx
00000007  cmp         dword ptr ds:[00952E14h],0
0000000e  je          00000015
00000010  call        7917A651
00000015  nop             

        // Foo bar
00000016  mov         ecx,1
0000001b  call        02A83E04
00000020  nop             
            Console.WriteLine(1);
            Console.WriteLine(2);
00000021  mov         ecx,2
00000026  call        02A83E04
0000002b  nop             
            Console.WriteLine(3);
0000002c  mov         ecx,3
00000031  call        02A83E04
00000036  nop             
        }
00000037  nop             
00000038  mov         esp,ebp
0000003a  pop         ebp 
0000003b  ret             

Stepping using F10, takes us first to the line with the {, then to the // Foo bar line, then to Console.WriteLine(2), and Console.WriteLine(3).

Of course, that begs the question of how you turn off the mapping, perhaps because the same IL instruction is used by two different lines of the high level source language. And that’s where 0xfeefee comes in. Using this as the line number turns off the mapping.

Changing the middle .line directive to use this special marker

IL_0007:  nop
.line 0xfeefee,0xfeefee : 13,34 ”
IL_0008:  ldc.i4.2
IL_0009:  call       void [mscorlib]System.Console::WriteLine(int32)

We now get the following disassembly

        static void Main(string[] args)
        {
00000000  push        ebp 
00000001  mov         ebp,esp
00000003  push        eax 
00000004  mov         dword ptr [ebp-4],ecx
00000007  cmp         dword ptr ds:[00952E14h],0
0000000e  je          00000015
00000010  call        7917A651
00000015  nop             

        // Foo bar
00000016  mov         ecx,1
0000001b  call        02A83E04
00000020  nop             
00000021  mov         ecx,2
00000026  call        02A83E04
0000002b  nop             
            Console.WriteLine(1);
            Console.WriteLine(2);
            Console.WriteLine(3);
0000002c  mov         ecx,3
00000031  call        02A83E04
00000036  nop             
        }
00000037  nop             
00000038  mov         esp,ebp
0000003a  pop         ebp 
0000003b  ret             

And the stepping happens as the line with  {, then the line with // Foo Bar followed by Console.WriteLine(3).

Hence it is the sequence points that give the necessary mapping information for the source level debugging. Depending on a variety of other factors, the debugging infrastructure inside the CLR only keeps this mapping approximate when JIT optimisations are allowed, leading to stepping behaviour that doesn’t always follow what the sequence points demand. Indeed some of the ngen optimisations don’t track the sequence points, and the DebuggableAttribute allows an assembly author to tell the system not to track all sequence points as possible step points. However, it amazing that the only information that the compiler needs to supply is the equivalent of the .line directives to allow this debugging to happen.

I should perhaps add that this sequence point information is encoded into the pdb file, into which you’d also add extra information to record local variable mappings. These files can be written using various COM interfaces that are present on Windows machines.

Advertisements
This entry was posted in Computers and Internet. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s