To understand a decompiler, you must first understand the compiler.
When you run luac my_script.lua, the Lua compiler does two things:
Example Source:
local x = 10
print(x + 5)
Compiled Bytecode (luac -l output):
main <stdin:0,0> (3 instructions)
1 [1] LOADK 0 -1 ; 10
2 [2] GETTABUP 1 0 -2 ; _ENV "print"
3 [2] ADD 2 0 -3 ; - 5
4 [2] CALL 1 2 1
A decompiler’s job is to reverse instruction 1 back into local x = 10. This is theoretically impossible to do perfectly because compilation loses information (comments, local variable names, if/while structure, and whitespace). lua decompiler
When you run luac -o script.luac script.lua, the compiler produces a binary file containing:
Let’s walk through the mental model of unluac. If you feed it bytecode, here is the simplified step-by-step:
Step 1: Disassembly
The decompiler reads the binary header (magic number, version, endianness) and walks through the list of prototypes (functions). It converts each bytecode instruction into a human-readable mnemonic (e.g., OP_MOVE, OP_ADD).
Step 2: Expression Extraction The decompiler scans backwards through the bytecode to build expressions. To understand a decompiler, you must first understand
Step 3: Block Partitioning (Basic Blocks)
The decompiler divides the instruction stream into "basic blocks"—straight-line code with no jumps in or out (except at the end). It finds JMP (goto) instructions and marks block boundaries.
Step 4: Control Flow Graph (CFG) Building The decompiler maps how blocks connect. If instruction 5 jumps to instruction 10, a line is drawn. This graph reveals the loops and conditionals.
Step 5: High-Level Structure Recovery (The Hard Part) Here is where decompilers guess. The CFG might look like:
The decompiler then outputs the reconstructed syntax. Example Source: local x = 10 print(x + 5)
Why is this imperfect? Consider a switch statement (if-elseif chain) versus a binary search tree of ifs. The bytecode looks identical. The decompiler must guess.
If you want to protect your Lua code, you cannot rely on compilation alone. Decompilers are too good. Instead, use obfuscators that actively confuse decompilation logic.
Lua, as a lightweight, high-level scripting language, is widely embedded in applications ranging from video games to network appliances. While the source code is often obfuscated or stripped in deployed applications, the underlying Lua Virtual Machine (LVM) executes a standardized bytecode. This paper explores the theoretical and practical challenges of Lua decompilation. We examine the architecture of the LVM, the structure of compiled chunks, the semantic gap between stack-based bytecode and register-based source code, and the modern arms race between decompilers and obfuscators.