Welcome to MSDN Blogs Sign in | Join | Help

Virtual code execution via IL interpretation

As Soma announced, we just shipped VS2010 Beta1. This includes dump debugging support for managed code and a very cool bonus feature tucked in there that I’ll blog about today.

Dump-debugging (aka post-mortem debugging) is very useful and a long-requested feature for managed code.  The downside is that with a dump-file, you don’t have a live process anymore, and so property-evaluation won’t work. That’s because property evaluation is implemented by hijacking a thread in the debuggee to run the function of interest, commonly a ToString() or property-getter. There’s no live thread to hijack in post-mortem debugging.

We have a mitigation for that in VS2010. In addition to loading the dump file, we can also interpret the IL opcodes of the function and simulate execution to show the results in the debugger.

 

Here, I’ll just blog about the end-user experience and some top-level points. I’ll save the technical drill down for future blogs.

Consider the following sample:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

public class Point 
{
    int m_x;
    int m_y;
    public Point(int x, int y)
    {
        m_x = x;
        m_y = y;
    }
    public override string ToString()
    {
        return String.Format("({0},{1})", this.X, this.Y);
    }

    public int X
    {
        get
        {
            return m_x;
        }
    }
    public int Y
    {
        get
        {
            return m_y;
        }
    }
}

public class Program
{
    static void Main(string[] args)
    {
        Dictionary<int, string> dict = new Dictionary<int, string>();
        dict[5] = "Five";
        dict[3] = "three";

        Point p = new Point(3, 4);
                
    }

    public static int  Dot(Point p1, Point p2)
    {
        int r2 = p1.X * p2.X + p1.Y * p2.Y;
        return r2;
    }

}

 

Suppose you have a dump-file from a thread stopped at the end of Main() (See newly added menu item “Debug | Save Dump As …”; load dump-file via “File | Open | File …”).

Normally, you could see the locals (dict, p) and their raw fields, but you wouldn’t be able to see the properties or ToString() values. So it would look something like this:

image

But with the interpreter, you can actually simulate execution. With the IL interpreter, here’s what it looks like in the watch window:

image

Which is exactly what you’d expect with live-debugging.  (In one sense, “everything still works like it worked before” is not a gratifying demo…)

The ‘*’ after the values are indications that they came from the interpreter.  Note you still need to ensure that property-evaluation is enabled in “Tools | options | Debugging”:

image

 

 

 

 

How does it work?
The Interpreter gets the raw IL opcodes via ICorDebug and then simulates execution of those opcodes. For example, when you inspect “p.X” in the watch window, the debugger can get the raw IL opcodes:

.method public hidebysig specialname instance int32
        get_X() cil managed
{
  // Code size       12 (0xc)
  .maxstack  1
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldfld      int32 Point::m_x
  IL_0007:  stloc.0
  IL_0008:  br.s       IL_000a
  IL_000a:  ldloc.0
  IL_000b:  ret
} // end of method Point::get_X

And then translate that ldfld opcode into a ICorDebug field fetch the same way it would fetch “p.m_x”. The problem gets a lot harder then that (eg, how does it interpret a newobj instruction?) but that’s the basic idea.

 

Other things it can interpret:

The immediate window is also wired up to use the interpreter when dump-debugging. Here are some sample things that work. Again, note the ‘*’ means the results are in the interpreter and the debuggee is not modified.

Simulating new objects:
? new Point(10,12).ToString()
"(10,12)"*

Basic reflection:
? typeof(Point).FullName
"Point"*

Dynamic method invocation:
? typeof(Point).GetMethod("get_X").Invoke(new Point(6,7), null)
0x00000006*

Calling functions, and even mixing debuggee data (the local variable ‘p’) with interpreter generated data (via the ‘new’ expression):
? Dot(p,new Point(10,20))
110*

 

It even works for Visualizers

Notice that it can even load the Visualizer for the Dictionary (dict) and show you the contents as a pretty array view rather than just the raw view of buckets. Visualizers are their own dll,  and we can verify that the dll is not actually loaded into the debuggee. For example, the Dictionary visualizer dll is  Microsoft.VisualStudio.DebuggerVisualizers.dll, but that’s not in the module list:

image

 

That’s because the interpreter has virtualized loading the visualizer dll into its own “virtual interpreter” space and not the actual debuggee process space. That’s important because in a dump file, you can’t load a visualizer dll post-mortem.

 

 

Other issues:

There are lots of other details here that I’m skipping over, like:

  1. The interpreter is definitely not bullet proof. If it sees something it can’t interpreter (like a pinvoke or dangerous code), then it simply aborts the interpretation attempt.
  2. The intepreter is recursive, so it can handle functions that call other functions. (Notice that ToString call get_X.)
  3. How does it deal with side-effecting operations?
  4. How does it handle virtual dispatch call opcodes?
  5. How does it handle ecalls?
  6. How does it handle reflection

 

Other advantages?

There are other advantages of IL interpretation for function evaluation, mainly that it addresses the ”func-eval is evil” problems by essentially degenerating dangerous func-evals to safe field accesses.

  1. It is provably safe because it errs on the side of safety. The interpreter is completely non-invasive (it operates on a dump-file!).
  2. No accidentally executing dangerous code.
  3. Side-effect free func-evals. This is a natural consequence of it being non-invasive.
  4. Bullet proof func-eval abort.
  5. Bullet proof protection against recursive properties that stack-overflow.
  6. It allows func-eval to occur at places previously impossible, such as in dump-files, when the thread is in native code, retail code, or places where there is no thread to hijack.

Closing thoughts

We realize that the interpreter is definitely not perfect. That’s part of why we choose to have it active in dump-files but not replace func-eval in regular execution. For dump-file scenarios, it took something that would have been completely broken and made many things work. 

Posted by jmstall | 8 Comments
Filed under: ,

MVP Summit 2009

For those going to the 2009 MVP Summit, I’ll be one of the speakers at the breakout sessions on March 2nd on Microsoft’s Main campus.

Posted by jmstall | 0 Comments

Managed Dump debugging support for Visual Studio and ICorDebug

This is the longest I've gone without blogging, but our PDC announcements have stuff way too cool to stay quiet about.

If you saw PDC, you've head that the CLR Debugging API, ICorDebug, will support dump-debugging. This enables any ICorDebug-based debugger (including Visual Studio and MDbg) to debug dump-files of .NET applications. The coolness goes well beyond that, but dump-debugging is just the easiest feature to describe.

This was not an overnight feature, and required some major architectural changes to be plumbed through the entire system.  Specifically, when dump-debugging, there's no 'live' debuggee, so you can't rely on a helper-thread running in the debuggee process to service debugging requests anymore, so you need a completely different model.

Rick Byers has an excellent description of the ICorDebug re-architecture in CLR 4.0.  He also describes some of the other advancements in the CLR Tools API space. Go read them.

Posted by jmstall | 4 Comments
Filed under:

Updated MSDN forums

The MSDN forums are updated and have a new look and feel. It's at a new link too:  http://forums.msdn.microsoft.com/en-US/netfxtoolsdev/threads/  (the old link still forwards).

Posted by jmstall | 2 Comments

Stuff in Reflection that's not in Metadata

Previously, I mentioned some things in Metadata that aren't exposed in Reflection.  Here's an opposite case.

While metadata represents static bits on disk, Reflection operates in a live process with access to the CLR's loader. So reflection can represent things the CLR loader and type system may do that aren't captured in the metadata.

 

For example, an array T[] may implement interfaces, but which set of interfaces is not captured in the metadata. So consider the following code that prints out the interfaces an int[] implements:

using System;

class Foo
{
    static void Main()
    {
        Console.WriteLine("Hi");
        Console.WriteLine(Environment.Version);
        Type[] t2 = typeof(int[]).GetInterfaces();
        foreach (Type t in t2)
        {
            Console.WriteLine(t.FullName);
        }
        Console.WriteLine("done");
    }
}

Compile it once for v1.0. Then run the same binary against v1 and v2.  It's the same file (and therefore same metadata) in both cases.
The V1 case is inconsistent because it doesn't show any interfaces, it should at least show a few builtins like IEnumerable (typeof(IEnumerable).IsAssignableFrom(typeof(int[])) == True). But in the v2 case, you see reflection showing the interfaces, particularly the new 2.0 generic interfaces, that the CLR's type system added to the the array. So the V2 list is not the same as the V1 list, but this difference is not captured in the metadata.

 

C:\temp>c:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\csc.exe t.cs
Microsoft (R) Visual C# .NET Compiler version 7.10.6001.4
for Microsoft (R) .NET Framework version 1.1.4322
Copyright (C) Microsoft Corporation 2001-2002. All rights reserved.


C:\temp>t.exe
Hi
1.1.4322.2407
done

C:\temp>set COMPLUS_VERSION=v2.0.50727

C:\temp>t.exe
Hi
2.0.50727.1433
System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.Generic.IList`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
System.Collections.Generic.ICollection`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
System.Collections.Generic.IEnumerable`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
done

Posted by jmstall | 0 Comments

Nice MSDN URLs

I noticed that MSDN finally has nice URLs for the BCL. (Or perhaps that should be "I finally noticed that ...", depending on how long this has been)

So instead of:

http://msdn.microsoft.com/en-us/library/1009fa28.aspx

You can do:

http://msdn.microsoft.com/en-us/library/system.reflection.assembly.loadfrom.aspx

For overloaded methods, it takes you to the disambiguation page.

This actually makes it a lot easier to find BCL APIs when you know the name. Now most of the time, I can just type the name right into the URL. And I have a higher confidence that this link won't get broken.

Posted by jmstall | 1 Comments

The price of complexity

My house was haunted. One of the lights would randomly go on or off and random times without anybody fiddling the switch.

The previous owner of our house had installed fancy dimmer light switches. On a whim, we replaced one of the fancy switches with a simple on/off switch.  As we took the old fancy switch out, we noticed that it had capacitors, a circuit board, resistors, even a microchip! That's a lot of different things to break down.

The new simple switch worked great, and the associated light no longer randomly toggles. So I conclude the fancy light switch was the problem.

Furthermore, the dimmer switches were more complicated to use because they had some fancy pressure touch thing: a quick fast press would toggle the light; a soft press would dim the light. It took us a while to get used to them.

When I was at Home Depot, the fancy switches were $40; whereas the simple on/off switches were ~$3.  The old simple switches throughout our house are still working; and we never really needed the dimming features for the fancy switches.

So the fancy switches were more expensive, harder to use, and haunted. Overall, not an ideal tradeoff for our needs. We'll stick with the simple on/off switches.

 

I think the same things applies to software design. You can build complicated software with lots of features, but that also means more places where it could break down. If a simple solution does the trick, it may not only be cheaper to build, but easier to use and cheaper to maintain in the long run.

Posted by jmstall | 4 Comments

Codegen for On Error Resume Next

VB has a "On Error Resume Next", which tells each line to swallow exceptions and just keep executing to the next line. It's kind of like a try-catch around every single line.

It may be tempting for C++ developers to make fun of VB for this, but this is a lot like programming with HRESULT (or any error handling strategy based on return codes) when you forget to check the return value. And as we look at the codegen below, VB's code could turn out to be even more efficient than the unmanaged error handling equivalent.

 

So in this VB snippet, the exception is is swallowed and "Next" is printed.

Module Module1

    Sub Main()
        On Error Resume Next
        Console.WriteLine("Hello!")
        Throw New Exception("Bong!")

        Console.WriteLine("Next")
    End Sub

End Module

 

If you're wondering what the codegen is like, you can always compile and then view the IL in ildasm. Or you can view it in Reflector as C#  instead of IL, which is easier to comprehend.

 

[STAThread]
public static void Main()
{
    // This item is obfuscated and can not be translated.
    int VB$ResumeTarget;
    try
    {
        int VB$CurrentStatement;
    Label_0001:
        ProjectData.ClearProjectError();
        int VB$ActiveHandler = -2;
    Label_0009:
        VB$CurrentStatement = 2;
        Console.WriteLine("Hello!");
    Label_0016:
        VB$CurrentStatement = 3;
        throw new Exception("Bong!");
    Label_0023:
        VB$CurrentStatement = 4;
        Console.WriteLine("Next");
        goto Label_009E;
    Label_0035:
        VB$ResumeTarget = 0;
        switch ((VB$ResumeTarget + 1))
        {
            case 1:
                goto Label_0001;

            case 2:
                goto Label_0009;

            case 3:
                goto Label_0016;

            case 4:
                goto Label_0023;

            case 5:
                goto Label_009E;

            default:
                goto Label_0093;
        }
    Label_0059:
        VB$ResumeTarget = VB$CurrentStatement;
        switch (((VB$ActiveHandler > -2) ? VB$ActiveHandler : 1))
        {
            case 0:
                goto Label_0093;

            case 1:
                goto Label_0035;
        }
    }
    catch (object obj1) when (?)
    {
        ProjectData.SetProjectError((Exception) obj1);
        goto Label_0059;
    }
Label_0093:
    throw ProjectData.CreateProjectError(-2146828237);
Label_009E:
    if (VB$ResumeTarget != 0)
    {
        ProjectData.ClearProjectError();
    }
}

 

 

So you can see it's just putting the entire region in a try/catch block, and then using a switch table to jump back to the appropriate line.  It has a "Current Statement" variable to track the last successful line to execute before an exception may have been thrown, and then switches to the next line on the exception path.

The switch table may seem evil at first, but remember that in native code, all those IfFailGoto() checks to propagate return results also add up to a lot of switching code. In this case, the branches are at least optimized into a single switch table as opposed to scattered branch code.

Posted by jmstall | 3 Comments

The waiting game

Punting on a problem can be good or bad, depending on the situation. Punting is not always retreating or surrendering.

Punting is good when the problem will be easier to solve later.

  1. For example, maybe you suspect something may happen that will render the problem moot (eg, a product would be discontinued). 
  2. Maybe you'll have more relevant information later that lets you inject some simplifying assumptions. 
  3. Maybe you'll have new technologies / options available later than make it easier to solve.
  4. Sometimes, some problems just work themselves out over time because the decisions to an ambiguous problem are answered by the decisions to a clear problem (see e^pi  = -1). 

 

Punting is bad when the problem gets harder to solver later.

  1. Maybe you're accumulating debt until you solve it. I find this is often the case with FxCop - I like to get FxCop enabled sooner rather than later because it's easier to stay at 0 errors once you get to 0 errors. 
  2. Debt accumulation also happens if future components will be broken until you fix some problem.
  3. Or maybe not having the problem solved is holding you hostage on solving other things.
  4. Maybe punting screws up your dependencies.
Posted by jmstall | 0 Comments

Understand the end-to-end scenarios

If you don't understand the end-to-end scenario, it's easy to do something that is ultimately self-defeating.

For example, my 3yr old daughter recently learned to play hide-and-seek. The goal of the game is to hide and avoid being found while the "it" player searches for you.

She's got the part about hiding down. But then once hidden, she shouts out "Daddy, come and find me!!". Given that I'm not deaf, it's kind of self-defeating.

 

But seriously...

This comes up often in performance scenarios. We've all heard some story where somebody tries a premature optimization and it ends up slowing things down. For example, somebody goes and adds caching to something, and the overhead of maintaining the cache drowns out any perf wins.

Another example is adding some cool feature that's an innate security hole.

A more general example is adding an feature X, but you need feature Y in order to use X.  For example, to create a new ICorDebug object in CLR V2, you needed to pass a version string of the debuggee. But V1 didn't have a way to get a version string from a running process, so we also had to add a 2nd new API, mscoree!GetVersionFromProcess in order to enable the attach scenarios.

Posted by jmstall | 1 Comments
Filed under:

Sometimes it's the obvious answer

Sometimes the answer to a question is so obvious that we skip over it looking for a fancier answer.

Example: A chair at my house had a bunch of little indentations on the seat - kind of like what you'd expect if somebody took a math compass and poked the chair a bunch of times.

I had some friends over and asked them what they thought caused the indentations. They guessed things like:

  • did you set something on top of it such as another chair?
  • did the buttons on somebody's pants scratch it?
  • did it get accidentally bumped a bunch of times?
  • did you drop a box of nails on it?

No. My 3 year old took a math compass and poked the chair a bunch of times.

 

But seriously ...
I noticed this with threading bugs. Sometimes I'd look at a crash dump of a race condition. I'd have no live repro and would be pouring over the sources trying to infer what the race could be. The search space for race conditions is huge, and I'd often find some crazy 15 step race ("the user presses 3 buttons within 10 ms while 4 threads happen to be at certain spots"). But while that was technically a bug, it turned out that was never the real problem. The real problem would always be much simpler.

Posted by jmstall | 1 Comments
Filed under:

Arguing by-example vs. by-principle

You can argue by providing examples supporting your case.  Alternatively, you can argue by appealing to more general principles.

For example, in arguing that "exposing public fields is bad," you could say:
By-principle: "It breaks abstraction and encapsulation."
By-example: "This untrusted plugin could set field m_foo to value 4 and cause a null-reference exception on line 16 of file widget.cs".

 

  By example By principle
When it's good Sometimes an example / counter-example can clearly illustrate a problem in an undeniable way. (example) Can rapidly prune the decision tree and avoid wasting time in overly specific discussions that would ultimately lead to a dead-end. 
Abstraction level low - uses specific data points high - uses generalizations
When it's bad This puts a burden on coming up with the "killer example", which you may not be able to do until it's too late. You can waste a lot of time trying to come up with specific examples. The inability to come up with a specific killer example can lead to a false sense of security. (Just because I can't find  a bug in your code does not mean it's bug-free.) Principles may be more subjective and may be more complicated to process.
Principles can be easily misapplied. (eg, "My principle is that our code should never crash; therefore I'm going to explicitly put null checks before every pointer access").
Myers-Briggs of target audience S (sensory) N (intuitive)
Posted by jmstall | 1 Comments
Filed under:

Things in Metadata that are missing in Reflection

System.Reflection is a high-level way of describing Types in .NET targetted at managed code consumers. The API is easy to use, but does not expose all the information that's actually present and affecting decisions.

For example, Reflection does not expose TypeRef, MemberRef, AssemblyRef, or other Ref tokens.  These tokens are references to things in other assemblies. Reflection just resolves them for you (potentially invoking an event to get help from your app) and hands back the resolved object.

Similarly, reflection is also missing TypeSpecs. TypeSpecs are just binary signature blobs that describe compound types (arrays, generics, etc). Reflection will parse  the blob and resolve it to a real System.Type.

This entry is not a complete list of all things missing in reflection; nor am I going to get into the other problems in reflection here. For now, I'll just look at TypeRefs...

Imagine you have a class that inherits from a type in another assembly. At the metadata level, the base type is described with a TypeRef token.

1. Practically, that means you could use reflection to inspect what a base type actually bound to,  but not what it was supposed to bind to (as described in the original assemblyRef).

2. Another issue is that when you have a high-level API (Reflection) that loses information over a low level API (IMetadataImport), you risk that the high-level API won't be able to talk to the low level API because it may not be able to provide it with the information the low level API requests.

3. In related trivia, ILDasm can print TypeRef, TypeSpec tokens:

//000010:         Console.WriteLine("Hi!" + arg);
  IL_0001:  ldstr      "Hi!" /* 70000001 */
  IL_0006:  ldarg.0
  IL_0007:  call       string [mscorlib/*23000001*/]System.String/*01000012*/::Concat(string,
                                                                                      string) /* 0A000010 */
  IL_000c:  call       void [mscorlib/*23000001*/]System.Console/*01000013*/::WriteLine(string) /* 0A000011 */
  IL_0011:  nop

So the inability to get the raw unresolved tokens from Reflection would prevent you from writing ILDasm on reflection that could print the above snippet.

Posted by jmstall | 1 Comments

Binary vs. Source compatibility

Binary Compatibility means that when something is updated, you continue to work without needing to even recompile. 

Source Compatibility means that you need to recompile to keep things working, but you don't have to actually change the sources.

One is not a superset of the other. Here are some examples in each combo.

Compatibility generally doesn't just mean that the change is discoverable, but that the change has some significant breaking implication that would cause a reasonably client to need to adjust their behavior. (Of course, you always find afterwards that's some important client did something unexpected and you need to compensate. Hence the heroics of the AppCompat folks). For example, with C# + reflection, any change is discoverable, so any change could technically break a client; but if a client breaks because they're relying on the names of private methods that you changed, they're hard-pressed to complain.

 

Yes Binary + Yes Source:

Renaming private methods. Changing a method body in a way that continues to behave the same.

 

Yes Binary,  Not Source:

Adding new method overloads.  Since overload resolution is determined at compile time, adding new methods won't affect already-compiled binaries. But if you recompile, it's possible that you may bind to the new overloads.  (For example, see float.Equals)

 

Not Binary, Yes Source:

In this case, you just need to recompile your sources to keep working. The compiler will respond to the change in a corrective way. For example, consider removing a method overload. At a binary level, the method you're bound to is removed and so things fail. But if you recompile, the compiler may bind to another overload that's semantically equivalent, and so things keep working without you having to change any source.

 

Not Binary, Not Source:

A real breaking change. This requires clients to update their sources and recompile. For example, removing a method.

Posted by jmstall | 2 Comments

Do you compile XML to IL?

We need some customer feedback to determine if we fix a regression that was added in VS2008.

Any language can target the CLR by compiling the language to IL, and then you immediately leverage the .NET platform, including access to the libraries and debugging tools.

Do you write a compiler that takes an XML source file in and then compiles it to IL, produces a managed PDB, and then expect to be able to debug the XML source file using the source-line information you put in the PDB?  For example, if MSBuild compiled to IL (instead of being interpreted), it would fall under this category.

Compilation techniques could mean:

  1. using Reflection.Emit and MarkSequencePoint
  2. compiling to C# source code with #line directives that refer back to the XML.
  3. emitting IL text files and using ilasm to produce
  4. using the unmanaged emit APIs directly.

 

What's regressed?

In VS2005, you can set breakpoints on source lines in the XML file (that map to the ranges specified in the PDB you emitted alongside the IL) and hit them. You can also do set-next-statement and do stepping.


In vS2008, setting code breakpoints in the XML file may not be hit. Instead, the IDE will inspect the source file contents to recognize it's XML and so ignore the managed PDB code ranges and attempt to set an xml data-breakpoint on the entire XML element. The data-breakpoint is designed to cooperate with the XML libraries, but not the managed PDBs. Thus the code breakpoint is not hit and you won't stop in the xml file. Does this impact you?

 

Example

Here's a very simple way to see the impact of this using case #2 above:

using System;
using System.Collections.Generic;
using System.Text;

namespace xml_debug
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hi!");
#line 4 "xmlfile1.xml"
            Console.WriteLine("ABC!");
            Console.WriteLine("DEF!");
            Console.WriteLine("GHI!");
#line default
            Console.WriteLine("Hi!");
        }
    }
}

File "xmlFile1.xml":

<?xml version="1.0" encoding="utf-8" ?>
<doc>
  <test>
    abc <-- line 4
    def
    ghi
  </test>
  <test>
    other
  </test>
</doc>

 

The #line directive in the C# file would cause the next lines (up to #line default) to be associated with lines in the XML file, thus having the PDB associate the xml file with the IL. You can try this out in both VS2005 and VS2008 as a default C# console application to get a feel for the differences and extent of the issue.

Posted by jmstall | 16 Comments
Filed under:
More Posts Next page »
 
Page view tracker