One command, a sibling output
MirrorVM.exe C:\apps\FileToProtect.dll
The tool writes C:\apps\FileToProtect_MVM.dll. Managed Framework executables can be passed as .exe inputs and get a sibling _MVM.exe. A modern .NET apphost is native; pass its managed DLL instead.
For each supported method, a generated per-type initializer builds and compiles its bytecode once, storing a decoded VirtualMachineProgram in a static field. The rewritten body loads that program, builds an object[] containing the original arguments, and calls VirtualMachine.Execute(VirtualMachineProgram, object[]). The stub casts or unboxes the returned object according to the method's declared return type. There is no separate typed entrypoint for strings or integers.
Supported methods are translated as a unit
At the M1 checkpoint, the converter accepted static, non-generic, non-void methods without locals or exception handlers, with at most 256 parameters. Supported parameter and return types were Boolean, the built-in integer types through 64-bit, IntPtr/UIntPtr, Single, Double, Decimal, and String. The linear CIL subset included argument loads, I4/I8/R4/R8 constants, ldstr, arithmetic, bitwise operations, shifts, comparisons, neg, not, ckfinite, all numeric conv.* opcodes, nop, and a final ret.
The converter builds a candidate bytecode sequence for a method and replaces the body only when the whole method passes those checks. If a method uses an unsupported opcode, its original body stays in place and the CLI reports the method and opcode. There is no partial rewrite. Calls and branches remain unsupported at this stage.
For example, the sample's branch and method-call bodies remain ordinary CIL and are reported as skipped. Supported arithmetic, string, and numeric bodies are replaced as complete methods. If an input has no supported methods, MirrorVM writes no protected assembly and prints the skip reasons.
Keep the runtime easy to inspect
The runtime has one contiguous VirtualMachineValue[] stack and one Push/Pop pair. Numeric values stay inline in the tagged value struct; object references use its reference field. The interpreter indexes a 256-entry handler table by byte opcode, and each handler's class name follows the CIL opcode it serves.
See the interpreter optimization note for before-and-after paths and what still allocates.
This structure is meant to be identifiable and traceable. It helps a reverse engineer follow a protected method from its rewritten stub, through bytecode dispatch, into the corresponding handler. MirrorVM includes no anti-debugging, anti-tamper, or concealment feature.
Verification and boundaries
The Release solution builds 25 configured sample targets. The protected sample matched its original CLR output on all 26 configured matrix entries in x86 and x64 processes, from Framework 2.0 through 4.8.1, Core 2.0 through 3.1, and .NET 5 through 10. All 25 unit tests pass on the .NET 9 x64 host. Framework 4.x targets ran on the installed 4.8.1 in-place CLR; those runs do not test separate historical 4.x runtimes.
Assemblies carrying a strong-name public key and mixed-mode assemblies are rejected; the tool does not accept a signing key to re-sign rewritten output. Native apphosts are also rejected. For modern .NET inputs, runtime/deps sidecars are renamed and updated, and a compatible MirrorVM.Runtime.dll is placed next to the result.
The virtualizer and unit tests target .NET 9. The runtime library builds for net20, netstandard2.0, and net9.0. The checked matrix covers the configured sample targets, while unsupported method features stay outside the current release scope.
The original and protected numeric and control-flow sample output matched on all 26 configured matrix entries in x86 and x64 processes. The sample also runs CLR-generated numeric opcode and floating-stack comparisons.