A Visual Basic 6 executable was produced in one of two ways, and what decompilation yields depends entirely on which. The binary carries the answer: there is nothing to guess.
MSVBVM60.dll. Typed, high-level instructions.The compilation mode is written in the file
VB6 offers two targets, set in the project properties under Compile. The chosen mode is not inferred after the fact: it occupies a dedicated field, and a single test reads it.
Native Code is the compiler's default setting, and it is by far what one meets in applications that were actually shipped. The free edition of VBReFormer reads that field and shows the mode as soon as the file is opened — the quickest way to know what to expect before going further.
What each mode gives back
P-Code
Each bytecode instruction corresponds to a language operation. The structure of the code comes back almost in full: loops, conditions, calls, string and Variant operations are recovered in their original form.
Reconstruction is largely mechanical, because the instruction stated explicitly what it did.
Native Code
Types are no longer declared; they follow from the way values are handled. The runtime supplies most of the evidence, a VB6 program delegating to it nearly everything that is not elementary arithmetic.
The result is more uneven from one procedure to the next, and this is the mode that covers the large majority of the binaries we are sent.
These gaps can be measured. Here are the rates of translation into a named VB6 construct, each with its denominator:
Measured on 207 VB6 programs, 125 compiled to native code and 82 to P-Code. The name-by-name detail is published.
What the result looks like
Here is real output, taken from our case study on a VB6 CrackMe. A single excerpt shows at once what the binary kept and what it lost.
Private Sub Command1_Click()
var_pv10 = Text1.Text
var_pv13 = Date$ & " " & Time$
For var_pv14 = 1 To Len(var_pv13) Step 1
If IsNumeric(Mid$(var_pv13, CLng(var_pv14), 1)) Then
var_pv15 = Asc(Mid$(var_pv13, CLng(var_pv14), 1))
If var_pv14 <= Len(var_pv10) Then
var_pv16 = Str(Asc(Mid$(var_pv10, CLng(var_pv14), 1)))
var_pv16 = Right$(var_pv16, 1)
End If
var_pv18 = var_pv18 & Chr$(CLng(var_pv15 + 17 + var_pv16))
End If
Next var_pv14
In cyan, what was recovered from the binary: the control name and its event handler (Command1_Click), the control name and its property (Text1.Text), and the runtime functions rendered under their original VB6 names — Len, IsNumeric, Mid$, Asc, Right$, Chr$.
In red, what was lost: the names of local variables. The decompiler gives them an identifier derived from their location, for want of anything better.
The algorithm is entirely legible: the validation key is built from the text entered, the date and the time. No local variable name is needed to understand it — but they will have to be renamed before recompiling.
What survives, what does not
This split does not depend on the compilation mode. It is a property of the format.
Kept in the binary
- Project, executable and help file names
- Names of forms, modules and classes
- Control names and their properties
- Names of public procedures
- Parameter names of public members
- Names of public member variables
- Name and library of
DeclareAPI calls Implementsinterfaces,WithEventsvariables
Absent from the binary
- Names of local variables
- Names of private procedures and variables
- Declared types of locals and parameters
- Constants and enumerations, folded onto their value
- Member names of a
Type - Parameter types and names of a
Declare - Comments and original formatting
It is the left-hand column that makes the difference between a navigable project and a heap of anonymous functions. A form recovers its name, its controls recover theirs, and public procedures stay identifiable — which is usually enough to take an application back over.
The test that misleads
One method circulates for guessing the mode without reading the field meant for it: examine the method table and check whether its entries look like valid code addresses. It fails exactly where it should help.
In a native binary that table holds pointers installed by the runtime, which have every appearance of being correct addresses. Following them amounts to decoding machine code as if it were bytecode, and produces hundreds of entries that correspond to nothing.
This is why a native binary handed to a tool built for P-Code does not return a clean error but an abundant, wrong result — the most expensive kind of failure, since it takes time to notice.
In practice
Identify the mode before anything else: it determines what is reachable. With P-Code, expect the structure of the code back almost as written. With Native Code, expect reconstructed code, accurate in its calls and operations, with local variable names to reassign.
In both cases the user interface is recovered in full — forms, controls, properties, resources — because it is described by data rather than by code. That is often what is actually wanted when taking over an application whose source code has been lost.