美文网首页
【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