我最近在参与Python字节码相关的工作,想与大家分享一些这方面的经验。更准确的说,我正在参与2.6到2.7版本的CPython解释器字节码的工作。
Python是一门动态语言,在命令行工具下运行时,本质上执行了下面的步骤:
当第一次执行到一段代码时,这段代码会被编译(如,作为一个模块加载,或者直接执行)。根据操作系统的不同,这一步生成后缀名是pyc或者pyo的二进制文件。 解释器读取二进制文件,并依次执行指令(opcodes)。Python解释器是基于栈的。要理解数据流向,我们需要知道每条指令的栈效应(如,操作码和参数)。
探索Python二进制文件
得到一个二进制文件字节码的最简单方式,是对CodeType结构进行解码:
import marshalfd = open('path/to/my.pyc', 'rb')magic = fd.read(4) # 魔术数,与python版本相关date = fd.read(4) # 编译日期code_object = marshal.load(fd)fd.close()
code_object包含了一个CodeType对象,它代表被加载文件的整个模块。为了查看这个模块的类定义、方法等的所有嵌套编码对象(编码对象,原文为code object),我们需要递归地检查CodeType的常量池。就像下面的代码:
import types def inspect_code_object(co_obj, indent=''):print indent, "%s(lineno:%d)" % (co_obj.co_name, co_obj.co_firstlineno)for c in co_obj.co_consts:if isinstance(c, types.CodeType):inspect_code_object(c, indent + ' ') inspect_code_object(code_object) # 从第一个对象开始
这个案例中,我们打印出一颗编码对象树,每个编码对象是其父对象的子节点。对下面的代码:
class A:def __init__(self):passdef __repr__(self):return 'A()'a = A()print a
我们得到的树形结果是:
<module>(lineno:2) A(lineno:2) __init__(lineno:3) __repr__(lineno:5)
为了测试,我们可以通过compile指令,编译一个包含Python源码的字符串,从而能够得到一个编码对象:
co_obj = compile(python_source_code, '<string>', 'exec')
要获取更多关于编码对象的信息,我们可以查阅Python文档的co_* fields 部分。
初见字节码
一旦我们得到了编码对象,我们就可以开始对它进行拆解了(在co_code字段)。从字节码中解析出它的含义:
? 解释操作码的含义 ? 提取任意参数dis模块的disassemble函数展示了是如何做到的。对我们前面例子,它输出的结果是:
2 0 LOAD_CONST 0 ('A') 3 LOAD_CONST 3 (()) 6 LOAD_CONST 1 (<code object A at 0x42424242, file "<string>", line 2>) 9 MAKE_FUNCTION 0 12 CALL_FUNCTION 0 15 BUILD_CLASS 16 STORE_NAME 0 (A) 8 19 LOAD_NAME 0 (A) 22 CALL_FUNCTION 0 25 STORE_NAME 1 (a) 9 28 LOAD_NAME 1 (a) 31 PRINT_ITEM 32 PRINT_NEWLINE 33 LOAD_CONST 2 (None) 36 RETURN_VALUE
新闻热点
疑难解答