lp: added NEWTABLE && SETLIST

- tables can now be (mostly) decompiled
- changed 'decompiled source' to 'pseudo-code' since the output doesn't typically match the compiled script source.
- misc. refactoring
This commit is contained in:
CPunch 2022-08-15 23:30:32 -05:00
parent 9da0d0ffbd
commit a248cc4807
3 changed files with 58 additions and 40 deletions

View File

@ -12,51 +12,48 @@ Lua has a relatively small instruction set (only 38 different opcodes!). This ma
```sh ```sh
> cat example.lua && luac5.1 -o example.luac example.lua > cat example.lua && luac5.1 -o example.luac example.lua
local total = 0 local tbl = {"Hello", "World"}
for i = 0, 9, 1 do print(tbl[1] .. " " .. tbl[2] .. ": " .. 2.5)
total = total + i
print(total)
end
> python main.py example.luac > python main.py example.luac
example.luac example.luac
==== [[example.lua's constants]] ==== ==== [[example.lua's constants]] ====
0: [NUMBER] 0.0 0: [STRING] Hello
1: [NUMBER] 9.0 1: [STRING] World
2: [NUMBER] 1.0 2: [STRING] print
3: [STRING] print 3: [NUMBER] 1.0
4: [STRING]
5: [NUMBER] 2.0
6: [STRING] :
7: [NUMBER] 2.5
==== [[example.lua's locals]] ==== ==== [[example.lua's locals]] ====
R[0]: total R[0]: tbl
R[1]: (for index)
R[2]: (for limit)
R[3]: (for step)
R[4]: i
==== [[example.lua's dissassembly]] ==== ==== [[example.lua's dissassembly]] ====
[ 0] LOADK : R[0] K[0] ; load 0.0 into R[0] [ 0] NEWTABLE : 0 2 0 ;
[ 1] LOADK : R[1] K[0] ; load 0.0 into R[1] [ 1] LOADK : R[1] K[0] ; load "Hello" into R[1]
[ 2] LOADK : R[2] K[1] ; load 9.0 into R[2] [ 2] LOADK : R[2] K[1] ; load "World" into R[2]
[ 3] LOADK : R[3] K[2] ; load 1.0 into R[3] [ 3] SETLIST : 0 2 1 ;
[ 4] FORPREP : R[1] 4 ; [ 4] GETGLOBAL : R[1] K[2] ; move _G["print"] into R[1]
[ 5] ADD : R[0] R[0] R[4] ; add R[4] to R[0], place into R[0] [ 5] GETTABLE : R[2] 0 K[3] ;
[ 6] GETGLOBAL : R[5] K[3] ; move _G["print"] into R[5] [ 6] LOADK : R[3] K[4] ; load " " into R[3]
[ 7] MOVE : 6 0 0 ; move R[0] into R[6] [ 7] GETTABLE : R[4] 0 K[5] ;
[ 8] CALL : 5 2 1 ; [ 8] LOADK : R[5] K[6] ; load ": " into R[5]
[ 9] FORLOOP : R[1] -5 ; [ 9] LOADK : R[6] K[7] ; load 2.5 into R[6]
[ 10] RETURN : 0 1 0 ; [ 10] CONCAT : 2 2 6 ; concat 5 values from R[2] to R[6], store into R[2]
[ 11] CALL : 1 2 1 ;
[ 12] RETURN : 0 1 0 ;
==== [[example.lua's decompiled source]] ==== ==== [[example.lua's pseudo-code]] ====
local total = 0.0
for i = 0.0, 9.0, 1.0 do
total = (total + i)
print(total)
end
local tbl = {}
tbl[1] = "Hello"
tbl[2] = "World"
print(tbl[1] .. " " .. tbl[2] .. ": " .. 2.5)
``` ```

View File

@ -8,7 +8,6 @@
from operator import concat from operator import concat
from subprocess import call from subprocess import call
from xmlrpc.client import Boolean
from lundump import Chunk, Constant, Instruction, Opcodes, whichRK, readRKasK from lundump import Chunk, Constant, Instruction, Opcodes, whichRK, readRKasK
class _Scope: class _Scope:
@ -63,7 +62,7 @@ class LuaDecomp:
# end the scope (if we're supposed too) # end the scope (if we're supposed too)
self.__checkScope() self.__checkScope()
print("\n==== [[" + str(self.chunk.name) + "'s decompiled source]] ====\n") print("\n==== [[" + str(self.chunk.name) + "'s pseudo-code]] ====\n")
for line in self.lines: for line in self.lines:
if self.annotateLines: if self.annotateLines:
@ -121,7 +120,7 @@ class LuaDecomp:
self.src = "" self.src = ""
# walks traceback, if local wasn't set before, the local needs to be defined # walks traceback, if local wasn't set before, the local needs to be defined
def __needsDefined(self, reg) -> Boolean: def __needsDefined(self, reg) -> bool:
for _, trace in self.traceback.items(): for _, trace in self.traceback.items():
if reg in trace.sets: if reg in trace.sets:
return False return False
@ -147,7 +146,7 @@ class LuaDecomp:
# if the top indx is a local, get it # if the top indx is a local, get it
return self.locals[indx] if indx in self.locals else self.top[indx] return self.locals[indx] if indx in self.locals else self.top[indx]
def __setReg(self, indx: int, code: str) -> None: def __setReg(self, indx: int, code: str, forceLocal: bool = False) -> None:
# if the top indx is a local, set it # if the top indx is a local, set it
if indx in self.locals: if indx in self.locals:
if self.__needsDefined(indx): if self.__needsDefined(indx):
@ -155,10 +154,9 @@ class LuaDecomp:
else: else:
self.__addExpr(self.locals[indx] + " = " + code) self.__addExpr(self.locals[indx] + " = " + code)
self.__endStatement() self.__endStatement()
elif self.aggressiveLocals: # 'every register is a local!!' elif self.aggressiveLocals or forceLocal: # 'every register is a local!!'
self.__newLocal(indx, code) self.__newLocal(indx, code)
self.__addSetTraceback(indx) self.__addSetTraceback(indx)
self.top[indx] = code self.top[indx] = code
@ -280,6 +278,13 @@ class LuaDecomp:
elif instr.opcode == Opcodes.SETTABLE: elif instr.opcode == Opcodes.SETTABLE:
self.__addExpr(self.__getReg(instr.A) + "[" + self.__readRK(instr.B) + "] = " + self.__readRK(instr.C)) self.__addExpr(self.__getReg(instr.A) + "[" + self.__readRK(instr.B) + "] = " + self.__readRK(instr.C))
self.__endStatement() self.__endStatement()
elif instr.opcode == Opcodes.NEWTABLE:
# i use forceLocal here even though i don't know *for sure* that the register is a local.
# this does help later though if the table is populated (which is 99% of the time). the other 1%
# only affects syntax and may look a little weird but is fine and equivalent non-the-less
# TODO: make this better
self.__setReg(instr.A, "{}", forceLocal=True)
elif instr.opcode == Opcodes.ADD: elif instr.opcode == Opcodes.ADD:
self.__emitOperand(instr.A, self.__readRK(instr.B), self.__readRK(instr.C), " + ") self.__emitOperand(instr.A, self.__readRK(instr.B), self.__readRK(instr.C), " + ")
elif instr.opcode == Opcodes.SUB: elif instr.opcode == Opcodes.SUB:
@ -353,5 +358,21 @@ class LuaDecomp:
elif instr.opcode == Opcodes.FORPREP: elif instr.opcode == Opcodes.FORPREP:
self.__addExpr("for %s = %s, %s, %s " % (self.__getLocal(instr.A+3), self.__getReg(instr.A), self.__getReg(instr.A + 1), self.__getReg(instr.A + 2))) self.__addExpr("for %s = %s, %s, %s " % (self.__getLocal(instr.A+3), self.__getReg(instr.A), self.__getReg(instr.A + 1), self.__getReg(instr.A + 2)))
self.__startScope("do", self.pc, instr.B) self.__startScope("do", self.pc, instr.B)
elif instr.opcode == Opcodes.SETLIST:
# LFIELDS_PER_FLUSH (50) is the number of elements that *should* have been set in the list in the *last* SETLIST
# eg.
# [ 49] LOADK : R[49] K[1] ; load 0.0 into R[49]
# [ 50] LOADK : R[50] K[1] ; load 0.0 into R[50]
# [ 51] SETLIST : 0 50 1 ; sets list[1..50]
# [ 52] LOADK : R[1] K[1] ; load 0.0 into R[1]
# [ 53] SETLIST : 0 1 2 ; sets list[51..51]
numElems = instr.B
startAt = ((instr.C - 1) * 50)
ident = self.__getLocal(instr.A)
# set each index (TODO: make tables less verbose)
for i in range(numElems):
self.__addExpr("%s[%d] = %s" % (ident, (startAt + i + 1), self.__getReg(instr.A + i + 1)))
self.__endStatement()
else: else:
raise Exception("unsupported instruction: %s" % instr.toString()) raise Exception("unsupported instruction: %s" % instr.toString())

View File

@ -152,7 +152,7 @@ class Constant:
self.data = data self.data = data
def toString(self): def toString(self):
return "[" + self.type.name + "] " + str(self.data) return "[%s] %s" % (self.type.name, str(self.data))
# format the constant so that it is parsable by lua # format the constant so that it is parsable by lua
def toCode(self): def toCode(self):
@ -164,7 +164,7 @@ class Constant:
else: else:
return "false" return "false"
elif self.type == ConstType.NUMBER: elif self.type == ConstType.NUMBER:
return str(self.data) return "%g" % self.data
else: else:
return "nil" return "nil"