PYC文件格式分析
Table of Contents
1. PYC 文件简介
不说废话,这里说的 pyc 文件就是 Python 程序编译后得到的字节码文件 (py->pyc)。
2. 基本格式
pyc 文件一般由 3 个部分组成:
- 最开始4个字节是一个 Maigc int,标识此 pyc 的版本信息,不同的版本的
Magic 都在
Python/import.c
内定义 - 接下来四个字节还是个 int,是 pyc 产生的时间 (TIMESTAMP,1970.01.01 到产生pyc时候的秒数)
- 接下来是个序列化了的 PyCodeObject(此结构在
Include/code.h
内定义),序列化方法在Python/marshal.c
内定义
前两个字段的读写很简单,接下来咱们主要看一下 PyCodeObject 的序列化过程,由于 PyCodeObject 内还有其他很多类型的 PyObject,所以咱们从一般的 PyObject 的序列化开始看起。
3. PyObject 的序列化
PyObject 的序列化在 Python/marshal.c
内实现,一般是先写入一个 byte
来标识此 PyObject 的类型,每种 PyObject 对应的类型也在
Python/marshal.c
内定义:
//Python/marshal.c:22 #define TYPE_NULL '0' #define TYPE_NONE 'N' #define TYPE_FALSE 'F' #define TYPE_TRUE 'T' #define TYPE_STOPITER 'S' #define TYPE_ELLIPSIS '.' #define TYPE_INT 'i' #define TYPE_INT64 'I' #define TYPE_FLOAT 'f' #define TYPE_BINARY_FLOAT 'g' #define TYPE_COMPLEX 'x' #define TYPE_BINARY_COMPLEX 'y' #define TYPE_LONG 'l' #define TYPE_STRING 's' #define TYPE_INTERNED 't' #define TYPE_STRINGREF 'R' #define TYPE_TUPLE '(' #define TYPE_LIST '[' #define TYPE_DICT '{' #define TYPE_CODE 'c' #define TYPE_UNICODE 'u' #define TYPE_UNKNOWN '?' #define TYPE_SET '<' #define TYPE_FROZENSET '>'
之后是 PyObject 的具体数据内容,变长的对象(str,tuple,list 等)往往还包含了一个 4 bytes 的 len,比如 PyIntObject 的存储可能是这样的:
'i' | 4 bytes int |
'I' | 8 bytes int |
而 PyStringObject 的存储是这样的:
's' | 4 bytes length | length bytes content(char[]) |
PyTupleObject 和 PyListObject 的存储分别是:
'(' | 4 bytes length | length 个 PyObject |
'[' | 4 bytes length | length 个 PyObject |
各种 PyObject 如何序列化,哪些内容被参与了序列化,可以参看
Python/marshal.c
内的函数 w_object
函数,接下来咱们着重看下前面提到的 PyCodeObject 的序列化。
4. PyCodeObject 的序列化
结构体 PyCodeObject 在 Include/code.h
中定义如下:
//Include/code.h typedef struct { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest doesn't count for hash/cmp */ PyObject *co_filename; /* string (where it was loaded from) */ PyObject *co_name; /* string (name, for reference) */ int co_firstlineno; /* first source line number */ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ } PyCodeObject;
我们再看 Python/marshal.c
里面的 w_object
函数,从中找出写
PyCodeObject 的部分如下:
//... else if (PyCode_Check(v)) { PyCodeObject *co = (PyCodeObject *)v; w_byte(TYPE_CODE, p); w_long(co->co_argcount, p); w_long(co->co_nlocals, p); w_long(co->co_stacksize, p); w_long(co->co_flags, p); w_object(co->co_code, p); w_object(co->co_consts, p); w_object(co->co_names, p); w_object(co->co_varnames, p); w_object(co->co_freevars, p); w_object(co->co_cellvars, p); w_object(co->co_filename, p); w_object(co->co_name, p); w_long(co->co_firstlineno, p); w_object(co->co_lnotab, p); } //...
根据上面两段代码,我们很容易就能看到 PyCodeObject 里哪些字段需要参数序列化了,我们就挨个解释下需要序列化的字段们:
co_argcount
:code需要的位置参数个数,不包括变长参数(*args 和 **kwargs)co_nlocals
:code内所有的局部变量的个数,包括所有参数co_stacksize
:code段运行时所需要的最大栈深度co_flags
:一些标识位,也在 code.h 里定义,注释很清楚,比如 CO_NOFREE(64) 表示此 PyCodeObject 内无 freevars 和 cellvars 等co_code
:PyStringObject('s'),code对应的字节码(参看Include/opcode.h
以及此文后续章节)co_consts
:所有常量组成的 tupleco_names
:code 所用的到符号表,tuple 类型,元素是字符串co_varnames
:code 所用到的局部变量名,tuple 类型,元素是 PyStringObject('s/t/R')co_freevars
:code 所用到的 freevar 的变量名,tuple 类型,元素是 PyStringObject('s/t/R')co_cellvars
:code 所用到的 cellvar 的变量名,tuple 类型,元素是 PyStringObject('s/t/R')co_filename
:PyStringObject('s'),此 code 对应的 py 文件co_name
:此 code 的名称co_firstlineno
:此 code 对应的 py 文件里的第一行的行号co_lnotab
:PyStringObject('s'),指令与行号的对应表
在 Python 代码中,每个作用域( 或者叫 block 或者名字空间? )对应一个 PyCodeObject 对象,所以会出现嵌套: 比如 一个 module 类 定义了 N
个 class,每个 class 内又定义了 M 个方法。 每个 子作用域 对应的
PyCodeObject 会出现在它的 父作用域 对应的 PyCodeObject 的
co_consts
字段里。
接下来用个例子对上面的某些字段做个说明,比如下面的 python 代码
#test.py import struct def abc(c): a = 1 b = 2 return struct.pack("S", a+b+c) var_a = "abc" var_b = {var_a : 123, "sec_key" : abc}
它编译后对应的 pyc 在 magic
和 time
后就放着一个 PyCodeObject 对象, 这个对象的个字段指如下:
字段 | 值 | 注释 |
---|---|---|
co_argcount | 0 | 模块没有参数 |
co_nlocals | 0 | 模块没有局部变量 |
co_stacksize | 3 | 栈最大尺寸 |
co_flags | 64 | CO_NOFREE |
co_code | 'd\x00\x00d…' | 字节码序列 |
co_consts | (-1, None, another-code-obj…) | 所有常量,包括模块里的 function,method |
co_names | ('struct', 'abc', 'var_a', 'var_b') | 此作用域内用到的所有符号 |
co_varnames | () | 局部变量名(模块没有局部变量) |
co_freevars | () | freevars |
co_cellvars | () | cellvars |
co_filename | "test.py" | 源码文件名 |
co_name | '<module>' | code 名字,模块名都是 <module>,class 是类名,func 是函数名 |
co_firstlineno | 3 | 此 code 对应作用域的第一行的行号 |
co_lnotab | '\x0c\x02\t\x05\x06\x01' | 行号表 |
这其中的 co_consts 里面的第三个元素是 function abc 的 PyCodeObject,我们来看下 function abc 的 code 的各字段:
字段 | 值 | 注释 |
---|---|---|
co_argcount | 1 | 1 个参数 c |
co_nlocals | 3 | 1 个参数 c,两个局部变量 a,b |
co_stacksize | 4 | 栈最大尺寸 |
co_flags | 67 | CO_OPTIMIZATION | CO_NEWLOCALS | CO_NOFREE |
co_code | 'd\x01\x00}…' | 字节码序列 |
co_consts | (None, 1, 2, 'S') | 函数里用到的所有常量 |
co_names | ('struct', 'pack') | 此作用域内用到的所有符号 |
co_varnames | ('c', 'a', 'b') | 局部变量名 |
co_freevars | () | freevars |
co_cellvars | () | cellvars |
co_filename | "test.py" | 源码文件名 |
co_name | 'abc' | "code 名字,func 是函数名" |
co_firstlineno | 5 | 此 code 对应作用域的第一行的行号 |
co_lnotab | '\x00\x01\x06\x01\x06\x01' | 行号表 |
6. co_freevars 与 co_cellvars
这两个字段是给 Closure 准备的( Python里没有真正的Closure ),通俗的说就是函数嵌套的时候用的到,比如:
def outter(o1,o2): fc1=o1+o2 fc2=o1*o2 def inner(i): return (fc1+fc2)*i
- 对于 outter 函数来说,他的局部变量 fc1 和 fc2 被它内部嵌套的函数所引用,则 fc1 和 fc2 变成它的 cellvars 而不是局部变量 varnames
- 对于 inner 函数,fc1 和 fc2 既不是局部变量也不是全局变量,他引用自外层函数,则 fc1 和 fc2 是 inner 的 freevars
7. PyStringObject 的序列化
通过前面的介绍,你可能会发现 PyCodeObject 里面的用到 str('s') 类型的地方很多,什么 co_consts 啊, co_names,co_varnames,co_freevars, co_cellvars 等等都是 str 的 tuple,里面的 str 重复的也比较多,要是一股脑这么写进去可能会占用很大空间,于是 w_object 里对 PyStringObject 的序列化又多加了两种类型:
- 't' :interned-string,暂时可能简单理解为 pyc 里回重复出现的 str,这个类型就是简单的把 str 的类型 's' 改成 't' 了,后面还是跟 length(4bytes) 和 content(char[])
- 'R' :指向 interned-string 的字符串引用,'R'后面跟 4 个 bytea 的引用序号
具体看 Python/marshal.c
内 w_object
的相关实现:
//... else if (PyString_CheckExact(v)) { if (p->strings && PyString_CHECK_INTERNED(v)) { PyObject *o = PyDict_GetItem(p->strings, v); if (o) { long w = PyInt_AsLong(o); w_byte(TYPE_STRINGREF, p); w_long(w, p); goto exit; } else { int ok; o = PyInt_FromSsize_t(PyDict_Size(p->strings)); ok = o && PyDict_SetItem(p->strings, v, o) >= 0; Py_XDECREF(o); if (!ok) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } w_byte(TYPE_INTERNED, p); } } else { w_byte(TYPE_STRING, p); } n = PyString_GET_SIZE(v); if (n > INT_MAX) { /* huge strings are not supported */ p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } w_long((long)n, p); w_string(PyString_AS_STRING(v), (int)n, p); } //...
8. 行号对照表
co_lnotab 可以看做是(字节码在 co_opcode 中的 index 增量(1 byte), 对应的源码的行号增量(1-bytes))顺次串成的的字节数组(字符串)。
9. 字段 co_code 与 Python 的 OPCODE
PyCodeObject 的 co_code 字段就是 python opcode 组成的序列,具体有哪些opcode 可以参看 Include/opcode.h
,这里面有些 opcode 有参数,有些没有参数,从 opcode.h 内的代码段:
#define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */ //... #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
可以看出,大于等与 90 的 opcode 是有参数的,有参数的 opcode 的参数是两个 unsigned byte,第一个是操作数,第二个目前固定为0x00但是不能省略,举例来说,我要把当前 code 的 co_consts 里的第二个常量(index 是 1)载入到栈顶,则对应的 opcode 序列为: |LOAD_CONST|0x01|0x00|
,也就是 0x640x010x00
。
其他的 opcode 大都类似,主要是对函数调用栈以及 co_consts, co_varnames,co_freevars,co_cellvars 的操作,还有些 BUILD_CLASS, BUILD_MAP,BUILD_LIST,BUILD_TUPLE,BUILD_SLICE,MAKE_FUNCTION, MAKE_CLOSURE 等构建对象的特殊指令。
有参数的 opcode 的参数的参数大多时候是个 index,比如 LOAD_CONST 1
的 1 就是个 index,表示把当前 PyCodeObject.co_consts[1]
这个常量载入到栈顶, LOAD_FAST 2
则是把 PyCodeObject.co_varnames[2]
这个局部变量载入到栈顶;而 MAKE_FUNCTION 2
则表示栈顶 code-obj
对应的
function 有两个默认参数。
前面 test.pyc
之外的东西,比如 class/closure 的创建,也都逃不过这些指令,具体每个指令的解释和用法可以参看:
http://docs.python.org/release/2.7/library/dis.html#python-bytecode-instructions
10. Discuss and Comment
Have few questions or feedback? Feel free to send me(killian.zhuo📧gmail.com) an email!