美文网首页
【Python】PyCodeObject与pyc文件

【Python】PyCodeObject与pyc文件

作者: lndyzwdxhs | 来源:发表于2018-12-05 17:07 被阅读20次

0x00 前言

  • py文件是如何执行的?
    • py代码不会转换成一系列的机器指令,然后让cpu去执行
    • Python的本质和Java/C#是一样的,都是通过虚拟机来执行字节码实现的
  • Python解释器是什么?
    • 就是平时调用的/usr/bin/python可执行文件
    • 它包含Python编译器和Python虚拟机
    • Python解释器在执行Python程序文件的时候。会首先使用Python编译器对源文件进行编译,编译会产生Python的一组字节码(byte code)(当然还会产生其他内容。。。);然后将编译结果交给Python虚拟机,由虚拟机一条一条顺序执行字节码
  • Python编译器和虚拟机在什么地方?
    • 没有在python.exe中,而是在python2*.dll动态库中,编译和执行的过程都调用了python2*.dll动态库
    • 对比javaa.java通过javac编译成a.class文件,然后通过java执行a.class中的字节码,得到执行结果
  • py源文件经过Python编译器后会产生什么?
    • 代码中操作类型的内容会生成字节码
    • 字符串、常量值和字节码等静态信息都会存储为一个PyCodeObject对象(Python运行时的对象);通过也会存储到一个文件中,就是pyc文件
    • PyCodeObject对象和pyc文件之间的关系就是序列化和反序列化的关系
      python interpreter

0x01 PyCodeObject

typedef struct {
    PyObject_HEAD
    int co_argcount;        /*Code Block位置参数的个数 #arguments, except *args */
    int co_nlocals;     /*Code Block中局部变量的个数,包括位置参数的个数 #local variables */
    int co_stacksize;       /*执行该段Code Block需要的栈空间 #entries needed for evaluation stack */
    int co_flags;       /* CO_..., see below */
    PyObject *co_code;      /*Code Block编译得到的字节码指令序列 instruction opcodes */
    PyObject *co_consts;    /*Code Block中的所有常量 list (constants used) */
    PyObject *co_names;     /*Code Block中的所有符号 list of strings (names used) */
    PyObject *co_varnames;  /*Code Block中局部变量名集合 tuple of strings (local variable names) */
    PyObject *co_freevars;  /*实现闭包需要的对象 tuple of strings (free variable names) */
    PyObject *co_cellvars;      /*Code Block中内部嵌套函数所引用的局部变量名集合 tuple of strings (cell variable names) */
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;  /*Code Block对应的py文件的完整路径 string (where it was loaded from) */
    PyObject *co_name;      /*Code Block的名字,通常是函数名或类名 string (name, for reference) */
    int co_firstlineno;     /*Code Block在对应的py文件中的起始行 first source line number */
    PyObject *co_lnotab;    /*字节码指令与py文件中源代码行号的对应关系 string (encoding addr<->lineno mapping) */
    void *co_zombieframe;     /* for optimization only (see frameobject.c) */
} PyCodeObject;
  • py文件中,一段code block都会产生一个PyCodeObject对象;
    • 怎样才算一段code block呢?其实从静态代码就可以观察出来(依据Python语法),属于一个作用域的就是一个code block,也可以说是名字空间(作用域是静态的,名字空间是运行时的)。

字节码

/* opcode.h */
#define STOP_CODE   0
#define POP_TOP     1
#define ROT_TWO     2
#define ROT_THREE   3
#define DUP_TOP     4
#define ROT_FOUR    5
#define NOP     9

#define UNARY_POSITIVE  10
#define UNARY_NEGATIVE  11
#define UNARY_NOT   12
#define UNARY_CONVERT   13

#define UNARY_INVERT    15

#define LIST_APPEND 18
#define BINARY_POWER    19

#define BINARY_MULTIPLY 20
#define BINARY_DIVIDE   21
#define BINARY_MODULO   22
#define BINARY_ADD  23
#define BINARY_SUBTRACT 24
#define BINARY_SUBSCR   25
#define BINARY_FLOOR_DIVIDE 26
#define BINARY_TRUE_DIVIDE 27
#define INPLACE_FLOOR_DIVIDE 28
#define INPLACE_TRUE_DIVIDE 29

#define SLICE       30
/* Also uses 31-33 */

#define STORE_SLICE 40
/* Also uses 41-43 */

#define DELETE_SLICE    50
/* Also uses 51-53 */

#define INPLACE_ADD 55
#define INPLACE_SUBTRACT    56
#define INPLACE_MULTIPLY    57
#define INPLACE_DIVIDE  58
#define INPLACE_MODULO  59
#define STORE_SUBSCR    60
#define DELETE_SUBSCR   61

#define BINARY_LSHIFT   62
#define BINARY_RSHIFT   63
#define BINARY_AND  64
#define BINARY_XOR  65
#define BINARY_OR   66
#define INPLACE_POWER   67
#define GET_ITER    68

#define PRINT_EXPR  70
#define PRINT_ITEM  71
#define PRINT_NEWLINE   72
#define PRINT_ITEM_TO   73
#define PRINT_NEWLINE_TO 74
#define INPLACE_LSHIFT  75
#define INPLACE_RSHIFT  76
#define INPLACE_AND 77
#define INPLACE_XOR 78
#define INPLACE_OR  79
#define BREAK_LOOP  80
#define WITH_CLEANUP    81
#define LOAD_LOCALS 82
#define RETURN_VALUE    83
#define IMPORT_STAR 84
#define EXEC_STMT   85
#define YIELD_VALUE 86
#define POP_BLOCK   87
#define END_FINALLY 88
#define BUILD_CLASS 89

#define HAVE_ARGUMENT   90  /* Opcodes from here have an argument: */

#define STORE_NAME  90  /* Index in name list */
#define DELETE_NAME 91  /* "" */
#define UNPACK_SEQUENCE 92  /* Number of sequence items */
#define FOR_ITER    93

#define STORE_ATTR  95  /* Index in name list */
#define DELETE_ATTR 96  /* "" */
#define STORE_GLOBAL    97  /* "" */
#define DELETE_GLOBAL   98  /* "" */
#define DUP_TOPX    99  /* number of items to duplicate */
#define LOAD_CONST  100 /* Index in const list */
#define LOAD_NAME   101 /* Index in name list */
#define BUILD_TUPLE 102 /* Number of tuple items */
#define BUILD_LIST  103 /* Number of list items */
#define BUILD_MAP   104 /* Always zero for now */
#define LOAD_ATTR   105 /* Index in name list */
#define COMPARE_OP  106 /* Comparison operator */
#define IMPORT_NAME 107 /* Index in name list */
#define IMPORT_FROM 108 /* Index in name list */

#define JUMP_FORWARD    110 /* Number of bytes to skip */
#define JUMP_IF_FALSE   111 /* "" */
#define JUMP_IF_TRUE    112 /* "" */
#define JUMP_ABSOLUTE   113 /* Target byte offset from beginning of code */

#define LOAD_GLOBAL 116 /* Index in name list */

#define CONTINUE_LOOP   119 /* Start of loop (absolute) */
#define SETUP_LOOP  120 /* Target address (relative) */
#define SETUP_EXCEPT    121 /* "" */
#define SETUP_FINALLY   122 /* "" */

#define LOAD_FAST   124 /* Local variable number */
#define STORE_FAST  125 /* Local variable number */
#define DELETE_FAST 126 /* Local variable number */

#define RAISE_VARARGS   130 /* Number of raise arguments (1, 2 or 3) */
/* CALL_FUNCTION_XXX opcodes defined below depend on this definition */
#define CALL_FUNCTION   131 /* #args + (#kwargs<<8) */
#define MAKE_FUNCTION   132 /* #defaults */
#define BUILD_SLICE     133 /* Number of items */

#define MAKE_CLOSURE    134     /* #free vars */
#define LOAD_CLOSURE    135     /* Load free variable from closure */
#define LOAD_DEREF      136     /* Load and dereference from closure cell */ 
#define STORE_DEREF     137     /* Store into cell */ 

/* The next 3 opcodes must be contiguous and satisfy
   (CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1  */
#define CALL_FUNCTION_VAR          140  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_KW           141  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_VAR_KW       142  /* #args + (#kwargs<<8) */

/* Support for opargs more than 16 bits long */
#define EXTENDED_ARG  143

0x02 pyc文件

  • 字节码指令和PyCodeObject对象(其中包含了字节码)都会存储到pyc文件中
  • 直接执行python test.py并不会产生pyc文件,import语句会触发生成pyc文件(执行到import语句时,先查看有没有对应的pyc文件,如果没有,会创建PyCodeObject对象,将对象写入文件中,接下来Python重新从pyc文件执行import动作)
  • py_compilecompiler标准库也可以生成pyc文件
  • pyc文件是二进制文件,想知道其中含义,需要知道PyCodeObject对象的结构,即每一个域的含义

pyc文件的读写

static void
write_compiled_module(PyCodeObject *co, char *cpathname, time_t mtime)
{
    FILE *fp;

    fp = open_exclusive(cpathname);
    if (fp == NULL) {
        if (Py_VerboseFlag)
            PySys_WriteStderr(
                "# can't create %s\n", cpathname);
        return;
    }
    /* ==写入magic number */
    PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);
    /* First write a 0 for mtime */
    /* ==写入时间信息 */
    PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
    /* ==写入PyCodeObject对象信息 */
    PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);
    if (fflush(fp) != 0 || ferror(fp)) {
        if (Py_VerboseFlag)
            PySys_WriteStderr("# can't write %s\n", cpathname);
        /* Don't keep partial file */
        fclose(fp);
        (void) unlink(cpathname);
        return;
    }
    /* Now write the true mtime */
    fseek(fp, 4L, 0);
    assert(mtime < LONG_MAX);
    PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);
    fflush(fp);
    fclose(fp);
    if (Py_VerboseFlag)
        PySys_WriteStderr("# wrote %s\n", cpathname);
}
  • 一个pyc文件会包含三个独立的信息:magic number、时间、PyCodeObject对象
    • magic number用来区分不同的Python版本,每个版本的magic number不同(为了兼容不同的Python版本,2.5版本不能import2.7pyc文件等情况)。
    • 时间信息用来保持pyc文件是最新的py文件生成的
    • PyCodeObject对象的写入
void
PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
{
    WFILE wf;
    wf.fp = fp;
    wf.error = 0;
    wf.depth = 0;
    wf.strings = NULL;
    wf.version = version;
    w_long(x, &wf);
}

void
PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
{
    WFILE wf;
    wf.fp = fp;
    wf.error = 0;
    wf.depth = 0;
    wf.strings = (version > 0) ? PyDict_New() : NULL;
    wf.version = version;
    w_object(x, &wf);
    Py_XDECREF(wf.strings);
}
  • 以上是将内容写入文件调用的函数
  • w_long()函数就是将数据一个字节一个字节写入文件中
  • w_object()函数的代码很长,但是逻辑很简单,内部其实就是针对不同的类型(intstring等)做不同的操作。但是最源头都是w_longw_string(毕竟所有的字符最后不是数字就是字母)。
  • Python在向pyc文件写入list的时候,只是将list中的数值或字符串写入文件;这也就意味着Python在加载pyc文件的时候,需要根据这些数值或字符串重新组装list
  • 写入每一种类型的数据时,都会先写入一个类型标识,要不然咋知道那一堆二进制字节流是什么意思呢。一个类型标识符的出现,预示着一个对象的开始,上一个对象的结束。
  • 类型标识符如下所示:
#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      '>'
  • 不论写入多复杂的对象,归结到最后都是数值的写入和字符串的写入。
  • 数值写入很简单,直接写入即可。
  • 字符串的写入相对复杂。

字符串写入pyc文件

typedef struct {
    FILE *fp;
    int error;
    int depth;
    /* If fp == NULL, the following are valid: */
    PyObject *str;
    char *ptr;
    char *end;
    PyObject *strings; /* dict on marshal, list on unmarshal */
    int version;
} WFILE;
  • WFILE结构就是对FILE文件句柄的封装,strings域需要重点关注一下,它在向pyc文件写入时,string会指向PyDictObject对象;而从pyc文件读出时,string则会指向PyListObject对象。

  • 写入pyc文件时,PyMarshal_WriteObjectToFile()函数就创建了PyDictObject对象(wf.strings = (version > 0) ? PyDict_New() : NULL;)。

  • string对象的键值对存储的结构为(PyStringObject, PyIntObject),PyIntObject对象表示的是第几个被写入到pyc文件的intern字符串(类似一个索引,到从pyc文件读取时,即string指向PyListObject对象时会用到)。

/* w_object中处理string的代码 */
else if (PyString_Check(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 = 1;
                    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 = 1;
            return;
        }
        w_long((long)n, p);
        w_string(PyString_AS_STRING(v), (int)n, p);
    }
  • 写入分为三种情况
    • 普通字符串:写入类型标识TYPE_STRINGw_long()写入字符串长度,w_string()写入字符串本身
    • intern字符串首次写入:string字典中加入字符串的键值对,写入类型标识符TYPE_INTERNED,后面和普通字符串一样
    • intern字符串非首次写入:写入类型标识TYPE_STRINGREF,然后w_long()写入string中查找到的key对应的value

pyc文件读出字符串

PyObject *
PyMarshal_ReadObjectFromFile(FILE *fp)
{
    RFILE rf;
    PyObject *result;
    rf.fp = fp;
    rf.strings = PyList_New(0);
    rf.depth = 0;
    rf.ptr = rf.end = NULL;
    result = r_object(&rf);
    Py_DECREF(rf.strings);
    return result;
}
  • 如上所示,pyc文件读取内容时,string被初始化为PyListObject对象
  • 进入r_object()r_objectw_object的逆运算),开始从pyc文件读取数据,创建PyCodeObject对象。
  • 当读取到TYPE_INTERNED标识符时,将标识符后面的字符串进行intern操作,然后将它加到string指向的PyListObject对象中;
  • 当读取到TYPE_STRINGREF标识符时,直接根据写入时存储的索引IDstring中查找已经intern的字符串

欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。


相关文章

网友评论

      本文标题:【Python】PyCodeObject与pyc文件

      本文链接:https://www.haomeiwen.com/subject/fikwcqtx.html