12.6. Nonintrusive Debugging
12.6.1. Introduction
When debugging is on, not only the corresponding .pdb
file gets processed during obfuscation but some of code optimizations are turned off to improve the interactive debugging experience for the resulting assembly. This leads to a bit different timing and size characteristics of the resulting code. The micro-changes in characteristics may mask or unmask the defects in your code, especially those tied to unpredictable factors such as time. The multithreaded deadlock is a canonical example of such a defect.
12.6.2. Sample Scenario
Let's take a look on concrete example. Suppose your obfuscated application suffers from intermittent multithreaded deadlock. You want to fix that. Everything you currently have is an obfuscated .exe
file without debugging information.
The next logical step is to find the source file names and line numbers where deadlock occurs. Being a quick and somewhat lazy person, you temporarily disable obfuscation for your assembly. Then you build it just to find out that deadlock does not occur anymore.
You think: “Hm... probably the issue is tied to that exact obfuscated file somehow”. You enable obfuscation again for that assembly. Then you build it. The deadlock manifests itself again.
You think: “OK, let's try debug
directive and then attach a debugger to the process”. You write:
[assembly: Obfuscation(Feature = "debug", Exclude = false)]
Then you build and run your project again only to find out that deadlock mysteriously does not occur anymore. How is that possible that a debug directive affects the runtime behavior?
The answer is debug directive does not affect the runtime behavior. It just induces slight changes in code speed and size. It turns out that those slight changes are enough to mask or unmask the multithreading defect in the code.
The nonintrusive
flag is designed to deal with this situation:
[assembly: Obfuscation(Feature = "debug [nonintrusive]", Exclude = false)]
It minimizes the amount of changes applied by Eazfuscator.NET to the assembly that are required to provide the debugging functionality. In this way, the assembly characteristics stay the same even when debugging is on. Now you get a reproducible defect together with debug information. So you build your project again and run it. The deadlock is reproduced.
What you do next is attach debugger to your running deadlocked process. Launch Visual Studio and use Debug → Attach to Process... (Ctrl+Alt+P) menu item. Then, select your process from the list of processes running on your machine.
Once the process is selected, the next step is to freeze all its running threads with Debug → Break All (Ctrl+Alt+Break) menu item. Then take a look at Debug → Windows → Threads (Ctrl+D,T) window and go through the threads one by one while examining their call stacks. Once you find the suspected call stacks please write down the file names and line numbers of possible deadlock locations.
You now have the information to proceed with a fix to your source code.
Stepping through the code (e.g. interactive debugging) is hugely limited when nonintrusive
flag is specified. Essentially you can only attach debugger to a process and freeze the threads as a last resort solution.