Saturday, July 30, 2011

DILE tutorial, part 5: Thread and Call Stack Panel

As I promised in the last entry, this time I'll explain why DILE has to load assemblies to make debugging work. But first a little history. When I started to write DILE I didn't know much about the Unmanaged Metadata API or the debugger API. In fact, I started DILE because I wanted to learn about these. Therefore during the development of the "disassembler" part I really didn't think of how debugging will work. But in the end I found out that I was quite lucky...

Using the Unmanaged Metadata API a lot of information can be read from assemblies similarly to Reflection. Each of these items (methods, types, properties, events, etc.) are identified by tokens which are 4 byte integers. And the debugger API is using these tokens as well. For example, the debugger API will just tell the type definition token of variables but then its the debugger's responsibility to find the matching metadata and make sense of it. If the debugger wants to ask the debugger API to evaluate a field for example, then again the debugger must give the field's token to the API. And luckily, I have all this information since I disassemble loaded assemblies anyway.

Hopefully this explains why you can't evaluate objects which reside in assemblies that are not loaded in DILE or why the call-stack is not complete until all necessary assemblies are loaded.

Obviously, Visual Studio needs such metadata information as well. And during debugging you can even see how VS is loading assemblies and analyzing them. One nice touch in DILE though is that loading all assemblies is not required. Of course, you can do it by enabling the "Warn if the debuggee loads an assembly which is not added to the DILE project?" option. But I prefer disabling this option and rather loading assemblies during debugging whenever I need them. DILE makes this quite easy by displaying an error message when an assembly is missing. In such cases usually you can just right-click the error message and choose "Add module to project" from the context menu to load the required assembly.

Back to the tutorial. I'll explain two panels this time. The first is the thread panel.

This panel simply lists all managed threads in the debuggee process. The active thread can be changed by double-clicking on the thread or by right-clicking and choosing "Change current thread" from the context-menu. In both cases the selected thread's state will be reflected in the UI which means that the thread's call stack, variables, arguments, etc. will be displayed (much like in Visual Studio).

And if you see a "<definition of the Thread class is not loaded>" message in the Name column then don't forget to load mscorlib.dll. This error message means that DILE was not able to retrieve the name of the thread because the necessary mscorlib assembly is not added to the project. The reason is explained above.

The second panel is more interesting. This is the Modules Panel.

It shows a list of all assemblies/modules loaded by the debuggee process. This panel allows loading assemblies by double-clicking them or right-clicking and selecting "Add module to the project" from the context menu.

If the debuggee loads assemblies from byte arrays or creates them on-the-fly (using Reflection.Emit or XSLT transformation or compiled regular expressions and so on) then those will be listed here as well. Assemblies which exist only in memory can be loaded as well just like any other assembly. In this case DILE will do something very nasty: it will load the assembly from the debuggee process by directly reading its memory. Such assemblies can be used just like any other assemblies otherwise: they can be disassembled, debugged, you can put breakpoints in their methods, etc. However, there is one small difference: when the debuggee stops running then in-memory assemblies will disappear from the DILE project as well since these exist only while the debuggee is alive and running.

Labels: ,