Debugabbility with Roundtripping Assemblies

I've gotten several questions about debugabbility IL round-tripping. Round-tripping is where you decompile an app (perhaps via ILDasm), potentially edit the IL, and then recompile it (perhaps via ILAsm). This is the backbone for rewriting an assembly, it's what my Inline IL tool does, and I notice Dotfuscator does it too. Roundtripping between ILasm and ILDasm is a core scenario for us on the CLR and we're very diligent about ensuring that works. You could also round-trip with other technologies like Reflection / Reflection-Emit.

Imagine your original app was written in C#. The debugabbility issues include:
1. when you ildasm your app, you don't want to lose the original C# source mapping information.
2. when you ilasm your app, you want the source mapping to match the original C#, even though now IL is technically your source language.
3. you want the JITter to generate the right sequence points so that the debugger can actually stop on the interesting IL points.

Here's step-by-step. Or if you're in a rush, you can just jump to the Summary at the end.
I show this for an .exe, but it would also work for a .dll. 

Disassemble  (Exe --> IL)

ILasm has .line (which is like C#'s #line) Use ildasm's "/linenum" flag to inject .line directives that map the IL back to the original source (eg, C#).

    ildasm MyApp.exe / /linenum

If you emit the /linenum flag, then the PDB will view the IL as the source. This can be used as a primitive way of debugging IL.

The /linenum switch is similar to the "/SOURCE" command line switch, but  /SOURCE just injects comments into the IL containing the original source. This is purely a readability thing and does not inject any directives that would change how the IL is (re)compiled.

Edit the IL:  (IL --> IL')

Manipuate the .il text file to do whatever transformation you were planning. You're on your own here. I hear you can go a long way with regular expressions.

Reassemble (IL' --> Exe)

ILasm has several key switches to choose from regarding debugabbility. These switches determine the DebuggableAttribute for the assembly, and correspond to C#'s '/debug' and '/optimize' switches. Rick explains how those map to the DA attribute here.  You may have noticed the DebuggableAttribute commented out in ILDasm; that's because it's determined from ILAsm command line switches and not the IL source.

Here are the different knobs set by the pertinent command line switches:

  • Jit Optimizations means whether the jit will optimize the code (such as inline IL).
  • Create a PDB means whether the ilasm will create a PDB.
  • Sequence points describe which IL locations the debugger can stop on. (Rick has a great description about sequence points here: ). Explicit means the JIT gets the sequence points from the PDB (which gives you the most control to set sequence points, but has the obvious drawback that you load the PDB while jitting even when you're not debugging). Implicit means the JIT infers the sequence points from certain patterns in the IL. See Rick's blog entry above for more details.

IL switch Jit Optimizations? Create PDB? Sequence. Points DebuggableAttribute raw bits
/PDB enabled Yes   none
/DEBUG disabled Yes Explicit (from PDB) ( 01 00 01 01 00 00 00 00 )
disabled Yes Implicit ( 01 00 03 01 00 00 00 00 )
/DEBUG=OPT enable Yes Implicit ( 01 00 03 00 00 00 00 00 )
(none of the above) enabled No   specified from IL file.

Note that I'm listing the DebuggableAttribute's raw bits for my own reference (I know I'm going to want to recall it; and what better place to keep that information than in a blog post with related info?)
You'll notice that none of these debuggability behaviors depends on whether a debugger is actually attached. That's because the mere presence of a debugger shouldn't change behavior. This is also why in .NET 2.0 (whidbey), we now always have "tracking debug info" on, and so you don't need to set it in the debug attribute. This is part of the reason that the DebuggableAttribute has changed from V1.1.

If you only care about debugging, then the conservative thing is to always use "/debug" because that will use explicit sequence points from the PDB, which will contain what you requested at the IL level.

In Summary:

If you want to do debugging IL, you can effectively just rewrite the PDB to associate the assembly with IL files instead of the original source (eg, C#). This means:
     ildasm MyApp.exe /
     ilasm /debug /output=MyApp.exe  

If you want to touch-up the assembly's IL while keeping it associated with the original source files, do:
     ildasm MyApp.exe / /linenum
     ... touch up ....
     ilasm /debug /output=MyApp.exe  


[Update:  1/23/05] Rick has pointed out that there are more outstanding issues about losing high-level language semantics that are actually encoded in the pdb (via something like custom attributes). Stay tuned for details.

Comments (5)

  1. Hi Mike,

    Could you give me any insights as to why code coverage doesn’t work with round tripping? I tried round tripping one of my assemblies, hoping that I would be able to see why foreach blocks don’t get %100 coverage. Unfortunately when I loaded the coverage file into Visual Studio, the .il file wasn’t nicely colorized. Any thoughts?

    Thanks, Jamie.

  2. Adam M. says:

    I’m using .NET 1.1, and it works here too! Almost…

    If I do the following:

    ildasm /linenum / MyDll.dll

    — edit —

    ilasm /dll /debug /out:MyDll.dll

    Then I can indeed step into the original source code instead of the MSIL, but the sequence points seem strange, like they were "inferred from patterns in the IL". It seems like it’s stopping on all stack-empty points or something, and the highlighting in Visual Studio (2003) is off as well.

    However, if I do this:

    ildasm /linenum / MyDll.dll

    — edit —

    ilasm /dll /out:MyDll.dll

    It works perfectly, with the source and sequence points from the original .dll. (The change is that I removed the /debug flag.)

    It’s unexpected that removing the /debug flag improves debugging, but hey. 😉 Perhaps things are different in 2.0?

  3. Question 1) What’s the return value from this C# function: static int Test() { int val = 1; try { return

  4. I mentioned in the recent dev lab that you can debug at the IL level . I demoed two ways to do this,

  5. Tips and Tricks – [ Rick Byers ] DebuggingModes.IgnoreSymbolStoreSequencePoints [ Mike Stall ] Debuggability

Skip to main content