Skip to main content
Standard Lua bytecode uses a fixed opcode numbering scheme that every disassembler and decompiler knows by heart. OP_MOVE is opcode 0 in Lua 5.1, OP_LOADK is opcode 1, and so on. Because these numbers are public and unchanging, any tool that reads Lua bytecode can immediately interpret the instruction stream. By remapping opcodes to different values and patching the Lua virtual machine to understand your custom numbering, you produce bytecode that standard tools cannot decode without first reverse engineering your VM modifications. This page explains how Lua opcodes work, how to remap them by modifying the Lua interpreter source, how to shuffle operand fields, and how to distribute a custom Lua runtime alongside your protected bytecode.

How Lua Opcodes Work

Lua instructions are 32-bit fixed-size values in most Lua versions. The bits are divided into an opcode field and operand fields that vary by instruction type.

Instruction Layout

In Lua 5.1, the instruction format uses the following bit layout: For instructions that need larger constants, the B and C fields can be combined into a single 18-bit Bx field, or a signed sBx field for jump offsets.

Standard Lua 5.1 Opcode Table

Lua 5.1 defines 38 opcodes numbered 0 through 37: Lua 5.4 expands this to 83 opcodes with new instructions for integer operations, constant table improvements, and more efficient closures.

The Remapping Technique

Opcode remapping requires modifying the Lua interpreter source code, recompiling both the compiler (luac) and the runtime (lua), and then using your custom toolchain to produce and execute bytecode.

Step 1: Modify the Opcode Enum

The opcode definitions live in lopcodes.h. In Lua 5.1, the enum looks like this:
To remap opcodes, shuffle the assigned values arbitrarily:
Every opcode must have a unique value in the valid range. If two opcodes share the same number, the VM will execute the wrong instruction and crash unpredictably.

Step 2: Recompile the Toolchain

After modifying lopcodes.h, rebuild both the compiler and the interpreter:
Your modified luac now emits bytecode with your custom opcode values. Your modified lua interpreter knows how to execute them. Standard luac and lua binaries will not be able to read or run your bytecode.

Step 3: Compile Scripts with Your Custom luac

When a standard decompiler like unluac reads your protected.luac, it interprets opcode 17 as OP_MOVE (which is correct in your mapping but wrong in standard Lua). Since standard tools expect opcode 0 for OP_MOVE, they misinterpret the entire instruction stream and produce garbage output.

Shuffle vs. Encrypt the Dispatch Table

A more aggressive variant goes beyond static remapping and adds runtime decoding to the VM’s instruction dispatch loop.

Static Remapping vs. Dynamic Decoding

Static remapping (the technique above) assigns fixed new numbers to each opcode. Dynamic decoding adds a translation step inside the VM loop:

Runtime-Keyed Decode Tables

You can key the decode table by a hash of the script content, so different scripts use different opcode mappings:
This means an analyst cannot create a universal decompiler for your bytecode. Each script requires its own decode table, which is derived from the script itself at load time.

Operand Field Shuffling

In addition to opcode remapping, you can rearrange the bit fields within the 32-bit instruction word.

Standard vs. Shuffled Bit Layout

Standard Lua 5.1 layout:
Shuffled layout (example: opcode in high bits):

Modifying the Bit Extraction Macros

The bit manipulation macros in lopcodes.h control how the compiler encodes and the VM decodes instructions:
To shuffle operand positions, redefine POS_A, POS_B, POS_C, and POS_OP:
Both luac (the compiler) and lvm.c (the VM) must use identical macros. If they differ by even one bit position, every instruction will be decoded incorrectly and the VM will crash.
Operand field shuffling requires modifying every instruction encoder and decoder in the Lua source. A single wrong bit field offset crashes every script. Test every instruction type thoroughly.

Distribution Implications

Opcode remapping requires distributing a custom Lua VM alongside your bytecode. This has important practical implications.

Standalone Lua Applications

For desktop or server applications, package your modified lua executable or liblua.so with your application:

Embedded Lua in Game Engines

Many game engines embed Lua for scripting. To use opcode remapping, you must modify the embedded Lua source and recompile the entire engine:
  1. Locate the Lua source inside the engine (often ThirdParty/lua/ or external/lua/)
  2. Apply your lopcodes.h and lvm.c modifications
  3. Rebuild the engine
  4. Compile game scripts with your custom luac

Roblox Luau Limitations

In Roblox Luau, you cannot replace the VM. The Luau interpreter is built into the Roblox client and server, and developers have no access to modify it. Opcode remapping is not possible in Roblox. For Roblox, use VM-based obfuscation implemented in Luau (see the Virtualization page) instead.

Partial Opcode Remapping Without Recompiling the VM

If you cannot replace the VM, you can still achieve opcode remapping by implementing a custom VM interpreter in Lua itself. Your custom bytecode uses your own opcode numbers, and a Lua-written VM decodes and executes them. This approach is what VM-based obfuscation does at its core. Instead of patching the C VM, you write a Lua VM in Lua:
This translation step is the foundation of the virtualization technique covered in detail on the Virtualization page.
Opcode remapping requires deep knowledge of Lua internals and careful testing across all instruction types. A single wrong bit field offset or opcode assignment crashes every script. Always test with comprehensive scripts that exercise every opcode.
This technique is what serious commercial game protection tools do under the hood. It is the most robust protection technique that does not require building a full custom VM from scratch.