DebuggableAttribute and dynamic assemblies

Mike Stall has a great little sample showing how to make your dynamically generated code debuggable.  However, there is one more detail you should be aware of.  By default the JIT compiler will enable optimizations for the module, making debugging difficult or (in the case of JMC-mode in VS) impossible.  If you run your program from within Visual Studio and have the “Suppress JIT optimizations on module load” option enabled, then it’ll work fine. However, I believe this setting in VS is now (as of the June CTP) disabled by default in order to minimize any changes to your program’s behaviour when run under the debugger.  Additionally, optimizations would also be enabled it you attached to an already running process.

Compilers like csc.exe have command line options such as /debug and /optimize that affect how the JIT generates code for the assembly.  The compiler communicates this information to the CLR by attaching a System.Diagnostics.DebuggableAttribute to the assembly.  This attribute contains a DebuggingModes value which is a bit-mask of various flags.  Here are the flags that the Whidbey C# compiler emits for the various combinations of compiler options:

/debug- Retail build: No attribute emitted (same as specifying DebuggingModes.None).  Setting /o+ or /o- doesn’t change anything when debug mode is disabled.  /debug- is the default and so can be omitted.
/debug+ Debug build: Default, IgnoreSybmolStoreSequencePoints, EnableEditAndContinue DisableOptimizations.  /o- is the default here.
/debug+ /o+ Debug optimized: Default, IgnoreSybmolStoreSequencePoints.  In Whidbey this is essentially the same thing as /debug-.

Note that before Whidbey, the “Default” flag was used to “enable JIT tracking”.  JIT tracking meant we would track information necessary for debugging such as IL/native maps (for converting native address offsets to IL offsets and vice versa).  In Whidbey we have an efficient form of JIT tracking enabled all of the time, so this flag is now basically meaningless.  However, we need to ensure the Whidbey CLR runs Everett binaries in the same way, so for backwards compatibility we still require the Default flag to be present if we’re going to do things like disable JIT optimizations.

So, if you want to emit a dynamic assembly which is always debuggable (similar to using the C# compiler’s /debug+ option), then you need to use the following code after calling DefineDynamicAssembly:

// Add a debuggable attribute to the assembly saying to disable optimizations
Type daType = typeof(DebuggableAttribute);
ConstructorInfo daCtor = daType.GetConstructor(new Type[] { typeof(DebuggableAttribute.DebuggingModes) });
CustomAttributeBuilder daBuilder = new CustomAttributeBuilder(daCtor, new object[] {
DebuggableAttribute.DebuggingModes.DisableOptimizations |
DebuggableAttribute.DebuggingModes.Default });
assemblyBuilder.SetCustomAttribute(daBuilder);

Note that it’s best to do this before calling DefineDynamicModule.  Visual Studio, for example, looks at the optimization setting by calling ICorDebugModule2.GetJITCompilerFlags as soon as it gets a LoadModule event (which is triggered by DefineDynamicModule).  If you haven’t emitted the DebuggableAttribute yet, VS will think optimizations are enabled and will prevent debugging of the module if JMC is enabled.

It’s amazing how complicated the concept of “debug build” can be here, especially when you add in backwards compatibility, debugger overrides, various config file overrides, etc.  Thankfully, the story is simpler in Whidbey since we no longer have to worry about JIT tracking.