Archives | Categories

PYC文件格式分析

Table of Contents

<2010-12-14 Tue>

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 :所有常量组成的 tuple
  • co_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 在 magictime 后就放着一个 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' 行号表

5. 图解 test.py 与 test.pyc 的结构关系


图一

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.cw_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!

Copyright © KDr2, SOME RIGHTS RESERVED UNDER CC BY-NC 4.0.

Built with Emacs 29.0.50 (Org mode 9.5.2).

Last updated: 2022-01-28 Fri 13:42.