VBReFormer is twenty years old. Its first version already decompiled native Visual Basic 6 at a time when we were being told that "native VB6 simply cannot be decompiled". Since then, every strange binary, every compiler idiom, every oddity of MSVBVM60 has eventually found its way into the product.
V7 keeps that knowledge and throws away everything else.
The engine has been rewritten in full, in .NET, from zero. Not a port: a rewrite, in which only the semantics — ten years of observing the VB6 runtime — were carried over. And for the first time, native and P-code are handled by the same product, to the same standard of finish.
What actually changes
1. Native, finally treated as a real decompilation problem
The old engine worked by pattern recognition: it knew what the code VB6 emits looks like, and it rendered it. That works beautifully — right up to the binary compiled with an unusual optimisation profile.
V7 works differently. It abstractly interprets the machine code: registers, virtual memory, FPU stack, tracking both the literal value and the expression that produced it. Disassembly goes through Iced. The result is an expression tree, not a recognised string.
The difference shows on the hard cases:
- all four VB6 optimisation profiles (Fast Code, Favor Small Code, No Optimization, Remove Safety Checks) are covered and measured separately — and, counter-intuitively, it is No Optimization that is hardest
- the jump tables of a
Select Caseare followed, including when aCasebody has no obvious incoming edge - the Variant model is reconstructed (tag, value,
ByRef) instead of guessed - UDTs on the stack, bounded arrays,
CurrencyandParamArrayare reconstructed as types, not as bytes
2. P-code, in the same product
A VB6 binary is either native or P-code, never both — and until now that meant two paths and two levels of quality. V7 decompiles P-code end to end: bytes → typed opcodes → stack machine → expression tree → control-flow graph → structuring → VB6 source.
It is the same structuring stage that serves both natures. A Do While … Loop rebuilt from P-code and the same one rebuilt from native go through the same code — and therefore through the same fixes.
3. Names
This is what separates a usable decompiler from an assembly dump. V7 restores:
- the members of
Me, publics, arguments,Staticvariables - calls into the
MSVBVM60runtime rendered as VB idioms (Left(s, 3), not__vbaStrLeft) — 635 ordinals catalogued - virtual calls (
VCall), including into theVB6.OLBintrinsics New→ the name of the class being instantiated- form inheritance (
Form→_Form) and control accessors - the bounds and element type of arrays, read from procedure descriptors and IIDs
Above all, V7 always says what it does not know. A name read from the binary is a fact; a proc_405630 is an unnamed location. The symbol inventory counts them separately — on SETUP1.EXE: 147 names read against 1,182 synthesised locations. So you know, line by line, what you can lean on.
4. Types
A complete type model, with a lattice, propagated through the body, the signature and the conditions. Properties are typed by aligning Property Get with Property Let. The return type of a TypeLib member is read from the TypeLib — never guessed from its name.
5. What the binary does not contain is written down
Reference=, the nine compilation options, StartMode=: none of these are in any structure of the VB6 format. The old product inferred them — sometimes well, sometimes not. V7 leaves a reasoned note in their place, saying why the value is missing.
Less pretty. True.
6. The reference tables
Ten years of reverse engineering MSVBVM60.DLL and VB6.EXE, frozen into the product — none of it derived from third-party work:
| Table | Entries |
|---|---|
| P-code machine opcodes | 1,536 |
| Control properties (31 types) | 1,212 |
| VB6 intrinsic constants | 711 |
MSVBVM60 runtime exports | 635 |
| Control members | 602 |
| Control events | 46 |
They are neither read from a CSV at startup nor downloaded: they are compiled into the assembly, indexed, and queried in constant time.
The tools
| Call graph | three scales (document, object, procedure), callers / callees filters, isolated-node hiding, drill-down navigation |
| Control flow | blocks, edges, loop headers, back edges, unreachable blocks dashed — and five representations of the content: shape, IR, VB6, assembly, P-code |
| Object and TypeLib explorer | answers "where is this defined?" when the answer is outside the binary: third-party OCX members, VBRUN constants, reconstructed signatures, the owning library |
| Win32 API browser | prototypes, aliases, notes — to fill in what a Declare lost |
| Symbol inventory | read vs synthesised names, filtering, global rename (call sites in other modules follow) |
| Designer | the GUI section of every form, 1,212 control property definitions across 31 types |
| Hex view | with a "bytes → code" path back |
| Resources | inventory, extraction |
| Binary patching | writing into the analysed executable |
| Project reconstruction | .vbp, .frm, .bas, .cls with the headers VB6 requires to reopen them |
| Bookmarks, global search, program and library finders |
The measurement bench
V7 is continuously held against three sets of ground truth: a corpus of 148 binaries we compiled ourselves together with their 377 original source files, the VB6 Setup Toolkit (whose sources Microsoft publishes), and a witness binary whose code we own.
A defect that has not been held against its source is not a defect: it is a hypothesis.
3,849 automated checks run on every change.
Legacy vs V7
Timings were taken on the same machine, on the same binaries.
| VBReFormer legacy | VBReFormer V7 | |
|---|---|---|
| Native decompilation | pattern recognition | abstract interpretation (registers + memory + FPU) |
| P-code decompilation | partial, separate path | complete, same structuring stage as native |
| Optimisation profiles covered | 1 (Fast Code) | 4, measured separately |
| Call graph for the whole document | absent | immediate, without decompiling |
| Runtime names resolved | not measured | 635 ordinals |
| Exported projects that recompile in VB6 | not measured | 27 / 108 |
| Automated checks | not measured | 3,849 |
| Interface languages | 1 | 11 |
The “not measured” cells are deliberate: we hold no reliable reading for the legacy engine on those three points, and an order of magnitude presented as a measurement would be exactly what this article holds against a decompiler.
One figure deserves to stand apart, because it measures V7 against itself: on the corpus of deliberately mistreated binaries, the locations where the engine wrote ? for want of being able to conclude went from 63 to 20. These are not corrected errors — they were already honest admissions. They are admissions that became unnecessary.
And the row that matters most remains recompilation. 27 projects out of 108 reopen and recompile as-is in the IDE. It is not a flattering number, and that is exactly why we publish it: it is the only criterion that measures what the customer actually wants.
What V7 refuses to do
A short list, and a deliberate one.
- No reconstructed
With. The compiler dissolves it; putting it back would mean inventing a structure the binary does not carry. Output is fully qualified (Frame1.Caption) - No plausible value. When a value cannot be rendered correctly, V7 writes
?. A credible, wrong number costs more than a visible marker - No invented name for something that has none.
proc_405630tells the truth; a pretty name would not
Availability
VBReFormer Online is in beta as of today — vb.decompiler.tools — and its free tier goes a long way: complete disassembly, complete P-code, structure, resources, designer, reconstructed .vbp, and the source as a real preview.
VBReFormer V7 (desktop) follows in the coming weeks. A desktop+cloud licence opens both.