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 Case are followed, including when a Case body has no obvious incoming edge
  • the Variant model is reconstructed (tag, value, ByRef) instead of guessed
  • UDTs on the stack, bounded arrays, Currency and ParamArray are 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, Static variables
  • calls into the MSVBVM60 runtime rendered as VB idioms (Left(s, 3), not __vbaStrLeft) — 635 ordinals catalogued
  • virtual calls (VCall), including into the VB6.OLB intrinsics
  • 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:

TableEntries
P-code machine opcodes1,536
Control properties (31 types)1,212
VB6 intrinsic constants711
MSVBVM60 runtime exports635
Control members602
Control events46

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 graphthree scales (document, object, procedure), callers / callees filters, isolated-node hiding, drill-down navigation
Control flowblocks, edges, loop headers, back edges, unreachable blocks dashed — and five representations of the content: shape, IR, VB6, assembly, P-code
Object and TypeLib exploreranswers "where is this defined?" when the answer is outside the binary: third-party OCX members, VBRUN constants, reconstructed signatures, the owning library
Win32 API browserprototypes, aliases, notes — to fill in what a Declare lost
Symbol inventoryread vs synthesised names, filtering, global rename (call sites in other modules follow)
Designerthe GUI section of every form, 1,212 control property definitions across 31 types
Hex viewwith a "bytes → code" path back
Resourcesinventory, extraction
Binary patchingwriting 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 legacyVBReFormer V7
Native decompilationpattern recognitionabstract interpretation (registers + memory + FPU)
P-code decompilationpartial, separate pathcomplete, same structuring stage as native
Optimisation profiles covered1 (Fast Code)4, measured separately
Call graph for the whole documentabsentimmediate, without decompiling
Runtime names resolvednot measured635 ordinals
Exported projects that recompile in VB6not measured27 / 108
Automated checksnot measured3,849
Interface languages111

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_405630 tells the truth; a pretty name would not

Availability

VBReFormer Online is in beta as of todayvb.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.