反编译"木兰编译器"并分析源码
大家都在说木兰编译器是在水项目,但我感觉很多人啥也不知道跟着黑,你随机抽样几个网友出来很有可能都解释不清楚Parser和lexer.所以我找到时间,拆开木兰编译器看看源码,是好是坏拆开看.
(写在前面)结果是:我觉得:木兰编译器给python换了个前端,但至少不是我原来想的加了层贴纸(靠eval实现那种),所以算是一个挺有趣的小项目,只是因为'国产编译器'名头太大了,再加上舆论,所以翻了车.但是要是我写出来这样一个项目(并且没有往项目里添加那些凑字数的文件的话),我是会很自豪的,至少它比我脱裤子放屁的pymips高到不知道哪里去了.
形象的说木兰编译器就是:有一群人找到个轮子,仔细读了读外胎的说明书,造了个外胎给轮子换上.内行觉得这外胎换了没啥意义,可能还没原来轮子好用,很多外行跟着起哄,以为木兰只是在原来的轮胎上贴了一层膜.
1. 反编译
易知,木兰编译器是用PyInstaller打包起来的python项目,于是反编译这个exe的思路就很清晰了
1.1 提取exe内容
用pyinstxtractor很容易就能提取PyInstaller生成的Windows可执行文件内容
python ./tools/pyinstxtractor.py ./ulang-0.2.2.exe
- 1
1.2 修补pyc文件
PyInstaller会把pyc文件的magic和时间戳吃掉,所以需要从struct文件里取前8个字节补回pyc文件前面.
# ./tools/add_header.py
import os
with open("./ulang-0.2.2.exe_extracted/struct", "rb") as f:
header = f.read()[:4]
for filename in os.listdir("./ulang-0.2.2.exe_extracted/PYZ-00.pyz_extracted"):
if 'ulang' not in filename:
continue
with open("./ulang-0.2.2.exe_extracted/PYZ-00.pyz_extracted/" + filename, "rb") as f:
data = f.read()
with open("./pyc/" + filename, "wb") as f:
f.write(header + data)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
mkdir ./pyc/
python ./tools/add_header.py
- 1
- 2
1.3 反编译pyc文件
用uncompyle6可以直接把pyc文件反编译了,至于下面的sh,我先ls一下,然后用熟练的列操作在vs code里粘出整齐的指令.
mkdir ./ulang/
mkdir ./ulang/codegen/
mkdir ./ulang/parser/
mkdir ./ulang/runtime/
pip install uncompyle6
uncompyle6 ./pyc/ulang.codegen.blockly.pyc > ./ulang/codegen/blockly.py
uncompyle6 ./pyc/ulang.codegen.pyc > ./ulang/codegen/__init__.py
uncompyle6 ./pyc/ulang.codegen.python.pyc > ./ulang/codegen/python.py
uncompyle6 ./pyc/ulang.codegen.ulgen.pyc > ./ulang/codegen/ulgen.py
uncompyle6 ./pyc/ulang.parser.core.pyc > ./ulang/parser/core.py
uncompyle6 ./pyc/ulang.parser.error.pyc > ./ulang/parser/error.py
uncompyle6 ./pyc/ulang.parser.lexer.pyc > ./ulang/parser/lexer.py
uncompyle6 ./pyc/ulang.parser.lrparser.pyc > ./ulang/parser/lrparser.py
uncompyle6 ./pyc/ulang.parser.parsergenerator.pyc > ./ulang/parser/parsergenerator.py
uncompyle6 ./pyc/ulang.parser.pyc > ./ulang/parser/__init__.py
uncompyle6 ./pyc/ulang.pyc > ./ulang/__init__.py
uncompyle6 ./pyc/ulang.runtime.env.pyc > ./ulang/runtime/env.py
uncompyle6 ./pyc/ulang.runtime.main.pyc > ./ulang/runtime/main.py
uncompyle6 ./pyc/ulang.runtime.pyc > ./ulang/runtime/__init__.py
uncompyle6 ./pyc/ulang.runtime.repl.pyc > ./ulang/runtime/repl.py
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
然后再手动调整一下就大功告成了!
这样反编译出来代码实际是跑不起来的,debug发现是有些地方出了小问题,这并不影响我阅读源码的大致思路.
2. 源码分析
2.1 项目结构
.
├── __init__.py
├── main.py
├── CodeGen
│ ├── __init__.py
│ ├── blockly.py
│ ├── python.py*
│ └── ulgen.py
├── parser
│ ├── __init__.py
│ ├── core.py
│ ├── error.py
│ ├── lexer.py
│ ├── lrparser.py*
│ ├── parsergenerator.py*
└── runtime
├── __init__.py
├── env.py
├── main.py
└── repl.py
*:是某个公开库的源文件副本
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
这个项目主要外部依赖于ast,rply,codegen.
2.2 ulang.parser
ulang.parser.core.Parser注释: A simple LR(1) parser to parse the source code of mu and yield the python ast for later using…(
一个简单的LR(1)解析器,用于解析mu的源代码并生成python ast供后续使用。)
我查了查资料,猜测作者应该熟读了rply的文档,基于文档指导实现了Parser和Lexer,以下为具体分析:
2.2.1 ulang.parser.lexer
ulang.parser.lexer选段:
lg.add('IDENTIFIER', '\\$?[_a-zA-Z][_a-zA-Z0-9]*')
lg.add('DOTDOTDOT', '\\.\\.\\.')
lg.add('DOTDOTLT', '\\.\\.<')
lg.add('DOTDOT', '\\.\\.')
lg.add('DOT', '\\.')
lg.add('DOLLAR', '\\$')
lg.add('[', '\\[')
lg.add(']', '\\]')
lg.add('(', '\\(')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我猜测: Lexer使用多次复制粘贴rply.LexerGenerator教程示例代码的方式,加上了所有词法规则,实现了词法分析.
2.2.2 ulang.parser.core
ulang.parser.core看起来比较硬核(毕竟是个core),充斥着大量函数,装饰器等,它基于rply.parsergenerator生成了一个Parser.
仔细读读rply文档,基本能够理解这个文件中的代码,整个文件思路很清晰,但是工作量比较大(和ulang.parser.lexer情况有点像).比如看似复杂冗长的装饰器,其实全部都是在用rply.parsergenerator.production来指定terminals (tokens) & non-terminals序列.
总的来说,我觉得作者在这一块应该付出了较大的精力.
2.2.3 ulang.parser.lrparser 和 ulang.parser.parsergenerator
这俩就很微妙了.我读的时候感觉风格有点奇怪,专门去查了下,结果发现就是rply.parser和rply.parsergenerator的副本
我只能猜测为凑字数(也可能是作者环境配不对没办法import)
2.2.4 ulang.parser.error
实现了一个SyntaxError,我认为中规中据.
2.3 ulang.CodeGen
ulang.CodeGen.blockly.CodeGen注释: A simple python ast to blockly xml converter.(一个简单的python ast到blockly xml的转换器)
和ulang.parser代码风格感觉不一样.ulang.CodeGen下的文件分别实现了:
ulang.CodeGen.ulgen: python ast -> ulangulang.CodeGen.blockly: python ast -> blockly xml
而ulang.CodeGen.python则是codegen.codegen的副本(换个人凑字数?)
总的来说,我觉得ulang.CodeGen比ulang.parser工作量差不多,但更有趣.
2.3.1 ulang.CodeGen.ulgen
照着codegen.codegen画瓢,再根据ulang的设想进行调整就能得到ulang.CodeGen.ulgen,其中工作量还是蛮大的.
2.3.2 ulang.CodeGen.blockly
这个文件是要把python ast转换成blockly xml,大概就是visit树节点,并根据节点类型作相应的转化,挺有趣而且工作量也蛮大的.
2.4 ulang.runtime
这个模块里代码尤其糟糕,充分发扬了整个项目的大力出奇迹风格,不太好表述,直接节选代码了:
ulang.runtime.repl节选:
# 遍历检查括号匹配(说起来你可能不信,[(])是匹配的)
unclosed = []
unmatched = [0, 0, 0]
last = 2 * ['']
for tok in tokens:
c = tok.gettokentype()
last[0], last[1] = last[1], c
if c in keywords:
unclosed.append(c)
if c == 'LBRACE':
unmatched[0] += 1
elif c == 'RBRACE':
unmatched[0] -= 1
if len(unclosed):
unclosed.pop(-1)
elif c == '(':
unmatched[1] += 1
elif c == ')':
unmatched[1] -= 1
elif c == '[':
unmatched[2] += 1
elif c == ']':
unmatched[2] -= 1
unmatched_sum = sum(unmatched)
unclosed_sum = len(unclosed)
if unclosed_sum > 0:
if unmatched_sum == 0:
if last[1] == 'NEWLINE':
if (last[0] == 'NEWLINE' or last[0]) == ';':
pass
return True
return unclosed_sum == 0 and unmatched_sum == 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
我感觉写成这样(对我来说)要快乐一些,还修了一个bug:
SYMBOLS = {"}": "{", "]": "[", ")": "(", "RBRACE": "LBRACE"}
last = [""] * 2
unmatched = []
unclosed = []
for tok in tokens:
c = tok.gettokentype()
last[0], last[1] = last[1], c
if c in keywords:
unclosed.append(c)
elif c == "RBRACE" and unclosed:
unclosed.pop()
if c in SYMBOLS.values(): # left/right symbol
unmatched.append(c)
elif c in SYMBOLS.keys() and unmatched.pop() != SYMBOLS[c]:
return False
return (
# NEWLINE
unclosed
and not unmatched
and last[1] == "NEWLINE"
and last[0] not in ["NEWLINE", ";"]
) or (
# closed and matched
not unclosed
and not unmatched
)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
ulang.runtime.env节选:
# 自带的功能(我还把它排整齐了)
return {
"print" : local_print,
"println" : lambda *objs: local_print(*objs, **{"end":"\n"}),
"assert" : local_assert,
"len" : len,
"enumerate" : enumerate,
"all" : all,
"any" : any,
"range" : range,
"round" : round,
"input" : input,
"reverse" : reversed,
"super" : super,
"locals" : lambda: locals(),
"bool" : bool,
"float" : float,
"int" : int,
"str" : str,
"list" : list,
"dict" : dict,
"set" : set,
"tuple" : lambda *args: args,
"char" : chr,
"ord" : ord,
"bytes" : lambda s, encoding="ascii":bytes(s, encoding),
"typeof" : lambda x: x.__class__.__name__,
"isa" : lambda x, t: isinstance(x, t),
"max" : max,
"min" : min,
"map" : map,
"filter" : filter,
"zip" : zip,
"staticmethod" : staticmethod,
"property" : property,
"ceil" : math.ceil,
"floor" : math.floor,
"fabs" : math.fabs,
"sqrt" : math.sqrt,
"log" : math.log,
"log10" : math.log10,
"exp" : math.exp,
"pow" : math.pow,
"sin" : math.sin,
"cos" : math.cos,
"tan" : math.tan,
"asin" : math.asin,
"acos" : math.acos,
"atan" : math.atan,
"spawn" : builtin_spawn,
"kill" : builtin_kill,
"self" : builtin_self,
"quit" : sys.exit,
"open" : open,
"install" : pip_install
