我最近在参与Python字节码相关的工作,想与大家分享一些这方面的经验。更准确的说,我正在参与2.6到2.7版本的CPython解释器字节码的工作。
Python是一门动态语言,在命令行工具下运行时,本质上执行了下面的步骤:
- 当第一次执行到一段代码时,这段代码会被编译(如,作为一个模块加载,或者直接执行)。根据操作系统的不同,这一步生成后缀名是pyc或者pyo的二进制文件。
- 解释器读取二进制文件,并依次执行指令(opcodes)。
Python解释器是基于栈的。要理解数据流向,我们需要知道每条指令的栈效应(如,操作码和参数)。
探索Python二进制文件
得到一个二进制文件字节码的最简单方式,是对CodeType结构进行解码:
1 2 3 4 5 6 |
; html-script: false ]import marshal fd = 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的常量池。就像下面的代码:
1 2 3 4 5 6 7 8 9 |
; html-script: false ]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) # 从第一个对象开始 |
这个案例中,我们打印出一颗编码对象树,每个编码对象是其父对象的子节点。对下面的代码:
1 2 3 4 5 6 7 |
; html-script: false ]class A: def __init__(self): pass def __repr__(self): return 'A()' a = A() print a |
我们得到的树形结果是:
1 2 3 4 |
<module>(lineno:2) A(lineno:2) __init__(lineno:3) __repr__(lineno:5) |
为了测试,我们可以通过compile指令,编译一个包含Python源码的字符串,从而能够得到一个编码对象:
1 |
co_obj = compile(python_source_code, '<string>', 'exec') |
要获取更多关于编码对象的信息,我们可以查阅Python文档的co_* fields 部分。
初见字节码
一旦我们得到了编码对象,我们就可以开始对它进行拆解了(在co_code字段)。从字节码中解析出它的含义:
• 解释操作码的含义
• 提取任意参数
dis模块的disassemble函数展示了是如何做到的。对我们前面例子,它输出的结果是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 15f976565833-5"> 12 CALL_FUNCTION 0 15/div> 我最近在参与Python字节码相关的工作,想与大家分享一些这方面的经验。更准确的说,我正在参与2.6到2.7版本的CPython解释器字节码的工作。 Python是一门动态语言,在命令行工具下运行时,本质上执行了下面的步骤:
Python解释器是基于栈的。要理解数据流向,我们需要知道每条指令的栈效应(如,操作码和参数)。 探索Python二进制文件 得到一个二进制文件字节码的最简单方式,是对CodeType结构进行解码:
code_object包含了一个CodeType对象,它代表被加载文件的整个模块。为了查看这个模块的类定义、方法等的所有嵌套编码对象(编码对象,原文为code object),我们需要递归地检查CodeType的常量池。就像下面的代码:
这个案例中,我们打印出一颗编码对象树,每个编码对象是其父对象的子节点。对下面的代码:
我们得到的树形结果是:
为了测试,我们可以通过compile指令,编译一个包含Python源码的字符串,从而能够得到一个编码对象:
要获取更多关于编码对象的信息,我们可以查阅Python文档的co_* fields 部分。 初见字节码 一旦我们得到了编码对象,我们就可以开始对它进行拆解了(在co_code字段)。从字节码中解析出它的含义: dis模块的disassemble函数展示了是如何做到的。对我们前面例子,它输出的结果是:
|