zoukankan      html  css  js  c++  java
  • py, pyc, pyw, pyo, pyd Compiled Python File (.pyc) 和Java或.NET相比,Python的Virtual Machine距离真实机器的距离更远

    https://my.oschina.net/renwofei423/blog/17404

    1.      PyCodeObject与Pyc文件

    通常认为,Python是一种解释性的语言,但是这种说法是不正确的,实际上,Python在执行时,首先会将.py文件中的源代码编译成Python的byte code(字节码),然后再由Python Virtual Machine来执行这些编译好的byte code。这种机制的基本思想跟Java,.NET是一致的。然而,Python Virtual Machine与Java或.NET的Virtual Machine不同的是,Python的Virtual Machine是一种更高级的Virtual Machine。这里的高级并不是通常意义上的高级,不是说Python的Virtual Machine比Java或.NET的功能更强大,更拽,而是说和Java或.NET相比,Python的Virtual Machine距离真实机器的距离更远。或者可以这么说,Python的Virtual Machine是一种抽象层次更高的Virtual Machine。

           我们来考虑下面的Python代码:

    [demo.py]

    class A:

        pass

    def Fun():

        pass

    value = 1

    str = “Python”

    a = A()

    Fun()

          

    Python在执行CodeObject.py时,首先需要进行的动作就是对其进行编译,编译的结果是什么呢?当然有字节码,否则Python也就没办法在玩下去了。然而除了字节码之外,还包含其它一些结果,这些结果也是Python运行的时候所必需的。看一下我们的demo.py,用我们的眼睛来解析一下,从这个文件中,我们可以看到,其中包含了一些字符串,一些常量值,还有一些操作。当然,Python对操作的处理结果就是自己码。那么Python的编译过程对字符串和常量值的处理结果是什么呢?实际上,这些在Python源代码中包含的静态的信息都会被Python收集起来,编译的结果中包含了字符串,常量值,字节码等等在源代码中出现的一切有用的静态信息。而这些信息最终会被存储在Python运行期的一个对象中,当Python运行结束后,这些信息甚至还会被存储在一种文件中。这个对象和文件就是我们这章探索的重点:PyCodeObject对象和Pyc文件。

    可以说,PyCodeObject就是Python源代码编译之后的关于程序的静态信息的集合:

    [compile.h]

    /* Bytecode object */ 

    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) */ 

    } PyCodeObject; 

    在对Python源代码进行编译的时候,对于一段Code(Code Block),会创建一个PyCodeObject与这段Code对应。那么如何确定多少代码算是一个Code Block呢,事实上,当进入新的作用域时,就开始了新的一段Code。也就是说,对于下面的这一段Python源代码:

    [CodeObject.py]

    class A:

        pass

    def Fun():

        pass

    a = A()

    Fun()

    在Python编译完成后,一共会创建3个PyCodeObject对象,一个是对应CodeObject.py的,一个是对应class A这段Code(作用域),而最后一个是对应def Fun这段Code的。每一个PyCodeObject对象中都包含了每一个代码块经过编译后得到的byte code。但是不幸的是,Python在执行完这些byte code后,会销毁PyCodeObject,所以下次再次执行这个.py文件时,Python需要重新编译源代码,创建三个PyCodeObject,然后执行byte code。

    很不爽,对不对?Python应该提供一种机制,保存编译的中间结果,即byte code,或者更准确地说,保存PyCodeObject。事实上,Python确实提供了这样一种机制——Pyc文件。

    Python中的pyc文件正是保存PyCodeObject的关键所在,我们对Python解释器的分析就从pyc文件,从pyc文件的格式开始。

    在分析pyc的文件格式之前,我们先来看看如何产生pyc文件。在执行一个.py文件中的源代码之后,Python并不会自动生成与该.py文件对应的.pyc文件。我们需要自己触发Python来创建pyc文件。下面我们提供一种使Python创建pyc文件的方法,其实很简单,就是利用Python的import机制。

    在Python运行的过程中,如果碰到import abc,这样的语句,那么Python将到设定好的path中寻找abc.pyc或abc.dll文件,如果没有这些文件,而只是发现了abc.py,那么Python会首先将abc.py编译成相应的PyCodeObject的中间结果,然后创建abc.pyc文件,并将中间结果写入该文件。接下来,Python才会对abc.pyc文件进行一个import的动作,实际上也就是将abc.pyc文件中的PyCodeObject重新在内存中复制出来。了解了这个过程,我们很容易利用下面所示的generator.py来创建上面那段代码(CodeObjectt.py)对应的pyc文件了。

    generator.py

    CodeObject.py

    import test

    print "Done"

    class A:

    pass

    def Fun():

    pass

    a = A()

    Fun()

    图1所示的是Python产生的pyc文件:

    可以看到,pyc是一个二进制文件,那么Python如何解释这一堆看上去毫无意义的字节流就至关重要了。这也就是pyc文件的格式。

    要了解pyc文件的格式,首先我们必须要清楚PyCodeObject中每一个域都表示什么含义,这一点是无论如何不能绕过去的。

    Field

    Content

    co_argcount

    Code Block的参数的个数,比如说一个函数的参数

    co_nlocals

    Code Block中局部变量的个数

    co_stacksize

    执行该段Code Block需要的栈空间

    co_flags

    N/A

    co_code

    Code Block编译所得的byte code。以PyStringObject的形式存在

    co_consts

    PyTupleObject对象,保存该Block中的常量

    co_names

    PyTupleObject对象,保存该Block中的所有符号

    co_varnames

    N/A

    co_freevars

    N/A

    co_cellvars

    N/A

    co_filename

    Code Block所对应的.py文件的完整路径

    co_name

    Code Block的名字,通常是函数名或类名

    co_firstlineno

    Code Block在对应的.py文件中的起始行

    co_lnotab

    byte code与.py文件中source code行号的对应关系,以PyStringObject的形式存在

    需要说明一下的是co_lnotab域。在Python2.3以前,有一个byte code,唤做SET_LINENO,这个byte code会记录.py文件中source code的位置信息,这个信息对于调试和显示异常信息都有用。但是,从Python2.3之后,Python在编译时不会再产生这个byte code,相应的,Python在编译时,将这个信息记录到了co_lnotab中。

    co_lnotab中的byte code和source code的对应信息是以unsigned bytes的数组形式存在的,数组的形式可以看作(byte code在co_code中位置增量,代码行数增量)形式的一个list。比如对于下面的例子:

    Byte code在co_code中的偏移

    .py文件中源代码的行数

    0

    1

    6

    2

    50

    7

    这里有一个小小的技巧,Python不会直接记录这些信息,相反,它会记录这些信息间的增量值,所以,对应的co_lnotab就应该是:0,1, 6,1, 44,5。

    2.      Pyc文件的生成

    前面我们提到,Python在import时,如果没有找到相应的pyc文件或dll文件,就会在py文件的基础上自动创建pyc文件。那么,要想了解pyc的格式到底是什么样的,我们只需要考察Python在将编译得到的PyCodeObject写入到pyc文件中时到底进行了怎样的动作就可以了。下面的函数就是我们的切入点:

    [import.c]

    static void write_compiled_module(PyCodeObject *co, char *cpathname, long mtime) 

        FILE *fp; 

        fp = open_exclusive(cpathname); 

        PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION); 

         

        /* First write a 0 for mtime */ 

        PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION); 

        PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION); 

         

        /* Now write the true mtime */ 

        fseek(fp, 4L, 0); 

        PyMarshal_WriteLongToFile(mtime, fp, Py_MARSHAL_VERSION); 

        fflush(fp); 

        fclose(fp); 

    这里的cpathname当然是pyc文件的绝对路径。首先我们看到会将pyc_magic这个值写入到文件的开头。实际上,pyc­_magic对应一个MAGIC的值。MAGIC是用来保证Python兼容性的一个措施。比如说要防止Python2.4的运行环境加载由Python1.5产生的pyc文件,那么只需要将Python2.4和Python1.5的MAGIC设为不同的值就可以了。Python在加载pyc文件时会首先检查这个MAGIC值,从而拒绝加载不兼容的pyc文件。那么pyc文件为什么会不兼容了,一个最主要的原因是byte code的变化,由于Python一直在不断地改进,有一些byte code退出了历史舞台,比如上面提到的SET_LINENO;或者由于一些新的语法特性会加入新的byte code,这些都会导致Python的不兼容问题。

    pyc文件的写入动作最后会集中到下面所示的几个函数中(这里假设代码只处理写入到文件,即p->fp是有效的。因此代码有删减,另有一个w_short未列出。缺失部分,请参考Python源代码):

    [marshal.c]

    typedef struct { 

        FILE *fp; 

        int error; 

        int depth; 

        PyObject *strings; /* dict on marshal, list on unmarshal */ 

    } WFILE; 

       

    #define w_byte(c, p) putc((c), (p)->fp) 

       

    static void w_long(long x, WFILE *p) 

        w_byte((char)( x      & 0xff), p); 

        w_byte((char)((x>> 8) & 0xff), p); 

        w_byte((char)((x>>16) & 0xff), p); 

        w_byte((char)((x>>24) & 0xff), p); 

       

    static void w_string(char *s, int n, WFILE *p) 

        fwrite(s, 1, n, p->fp); 

    在调用PyMarshal_WriteLongToFile时,会直接调用w_long,但是在调用PyMarshal_WriteObjectToFile时,还会通过一个间接的函数:w_object。需要特别注意的是PyMarshal_WriteObjectToFile的第一个参数,这个参数正是Python编译出来的PyCodeObject对象。

    w_object的代码非常长,这里就不全部列出。其实w_object的逻辑非常简单,就是对应不同的对象,比如string,int,list等,会有不同的写的动作,然而其最终目的都是通过最基本的w_long或w_string将整个PyCodeObject写入到pyc文件中。

    对于PyCodeObject,很显然,会遍历PyCodeObject中的所有域,将这些域依次写入:

    [marshal.c]

    static void w_object(PyObject *v, WFILE *p) 

    {

        …… 

        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); 

    …… 

    }

    而对于一个PyListObject对象,想象一下会有什么动作?没错,还是遍历!!!:

    [w_object() in marshal.c]

    …… 

    else if (PyList_Check(v)) 

        { 

            w_byte(TYPE_LIST, p); 

            n = PyList_GET_SIZE(v); 

            w_long((long)n, p); 

            for (i = 0; i < n; i++) 

            { 

                w_object(PyList_GET_ITEM(v, i), p); 

            } 

    }

    …… 

    而如果是PyIntObject,嗯,那太简单了,几乎没有什么可说的:

    [w_object() in marshal.c] 

    ……

    else if (PyInt_Check(v)) 

        { 

            w_byte(TYPE_INT, p); 

            w_long(x, p); 

        } 

    ……

    有没有注意到TYPE_LIST,TYPE_CODE,TYPE_INT这样的标志?pyc文件正是利用这些标志来表示一个新的对象的开始,当加载pyc文件时,加载器才能知道在什么时候应该进行什么样的加载动作。这些标志同样也是在import.c中定义的:

    [import.c]

    #define TYPE_NULL   '0' 

    #define TYPE_NONE   'N'

    。。。。。。 

    #define TYPE_INT    'i' 

    #define TYPE_STRING 's' 

    #define TYPE_INTERNED   't' 

    #define TYPE_STRINGREF  'R' 

    #define TYPE_TUPLE  '(' 

    #define TYPE_LIST   '[' 

    #define TYPE_CODE   'c' 

    到了这里,可以看到,Python对于中间结果的导出实际是不复杂的。实际上在write的动作中,不论面临PyCodeObject还是PyListObject这些复杂对象,最后都会归结为简单的两种形式,一个是对数值的写入,一个是对字符串的写入。上面其实我们已经看到了对数值的写入过程。在写入字符串时,有一套比较复杂的机制。在了解字符串的写入机制前,我们首先需要了解一个写入过程中关键的结构体WFILE(有删节):

    [marshal.c]

    typedef struct { 

        FILE *fp; 

        int error; 

        int depth; 

        PyObject *strings; /* dict on marshal, list on unmarshal */ 

    } WFILE; 

    这里我们也只考虑fp有效,即写入到文件,的情况。WFILE可以看作是一个对FILE*的简单包装,但是在WFILE里,出现了一个奇特的strings域。这个域是在pyc文件中写入或读出字符串的关键所在,当向pyc中写入时,string会是一个PyDictObject对象;而从pyc中读出时,string则会是一个PyListObject对象。

    [marshal.c]

    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;

        w_object(x, &wf); 

    可以看到,strings在真正开始写入之前,就已经被创建了。在w_object中对于字符串的处理部分,我们可以看到对strings的使用:

    [w_object() in marshal.c] 

    ……

    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 

                { 

                    o = PyInt_FromLong(PyDict_Size(p->strings)); 

                    PyDict_SetItem(p->strings, v, o); 

                    Py_DECREF(o); 

                    w_byte(TYPE_INTERNED, p); 

                } 

            } 

            else 

            { 

                w_byte(TYPE_STRING, p); 

            } 

            n = PyString_GET_SIZE(v); 

            w_long((long)n, p); 

            w_string(PyString_AS_STRING(v), n, p); 

    }

    ……

    真正有趣的事发生在这个字符串是一个需要被进行INTERN操作的字符串时。可以看到,WFILE的strings域实际上是一个从string映射到int的一个PyDictObject对象。这个int值是什么呢,这个int值是表示对应的string是第几个被加入到WFILE.strings中的字符串。

    这个int值看上去似乎没有必要,记录一个string被加入到WFILE.strings中的序号有什么意义呢?好,让我们来考虑下面的情形:

    假设我们需要向pyc文件中写入三个string:”Jython”, “Ruby”, “Jython”,而且这三个string都需要被进行INTERN操作。对于前两个string,没有任何问题,闭着眼睛写入就是了。完成了前两个string的写入后,WFILE.strings与pyc文件的情况如图2所示:

    在写入第三个字符串的时候,麻烦来了。对于这个“Jython”,我们应该怎么处理呢?

    是按照上两个string一样吗?如果这样的话,那么写入后,WFILE.strings和pyc的情况如图3所示:

    我们可以不管WFILE.strings怎么样了,但是一看pyc文件,我们就知道,问题来了。在pyc文件中,出现了重复的内容,关于“Jython”的信息重复了两次,这会引起什么麻烦呢?想象一下在python代码中,我们创建了一个button,在此之后,多次使用了button,这样,在代码中,“button”将出现多次。想象一下吧,我们的pyc文件会变得多么臃肿,而其中充斥的只是毫无价值的冗余信息。如果你是Guido,你能忍受这样的设计吗?当然不能!!于是Guido给了我们TYPE_STRINGREF这个东西。在解析pyc文件时,这个标志表明后面的一个数值表示了一个索引值,根据这个索引值到WFILE.strings中去查找,就能找到需要的string了。

    有了TYPE_STRINGREF,我们的pyc文件就能变得苗条了,如图4所示:

    看一下加载pyc文件的过程,我们就能对这个机制更加地明了了。前面我们提到,在读入pyc文件时,WFILE.strings是一个PyListObject对象,所以在读入前两个字符串后,WFILE.strings的情形如图5所示:

    在加载紧接着的(R,0)时,因为解析到是一个TYPE_STRINGREF标志,所以直接以标志后面的数值0位索引访问WFILE.strings,立刻可得到字符串“Jython”。

    3.      一个PyCodeObject,多个PyCodeObject?

    到了这里,关于PyCodeObject与pyc文件,我们只剩下最后一个有趣的话题了。还记得前面那个test.py吗?我们说那段简单的什么都做不了的python代码就要产生三个PyCodeObject。而在write_compiled_module中我们又亲眼看到,Python运行环境只会对一个PyCodeObject对象调用PyMarshal_WriteObjectToFile操作。刹那间,我们竟然看到了两个遗失的PyCodeObject对象。

    Python显然不会犯这样低级的错误,想象一下,如果你是Guido,这个问题该如何解决?首先我们会假想,有两个PyCodeObject对象一定是包含在另一个PyCodeObject中的。没错,确实如此,还记得我们最开始指出的Python是如何确定一个Code Block的吗?对喽,就是作用域。仔细看一下test.py,你会发现作用域呈现出一种嵌套的结构,这种结构也正是PyCodeObject对象之间的结构。所以到现在清楚了,与Fun和A对应得PyCodeObject对象一定是包含在与全局作用域对应的PyCodeObject对象中的,而PyCodeObject结构中的co_consts域正是这两个PyCodeObject对象的藏身之处,如图6所示:

    在对一个PyCodeObject对象进行写入到pyc文件的操作时,如果碰到它包含的另一个PyCodeObject对象,那么就会递归地执行写入PyCodeObject对象的操作。如此下去,最终所有的PyCodeObject对象都会被写入到pyc文件中去。而且pyc文件中的PyCodeObject对象也是以一种嵌套的关系联系在一起的。

    4.      Python字节码

    Python源代码在执行前会被编译为Python的byte code,Python的执行引擎就是根据这些byte code来进行一系列的操作,从而完成对Python程序的执行。在Python2.4.1中,一共定义了103条byte code:

    [opcode.h]

    #define STOP_CODE   0

    #define POP_TOP     1

    #define ROT_TWO     2

    ……

    #define CALL_FUNCTION_KW           141

    #define CALL_FUNCTION_VAR_KW       142

    #define EXTENDED_ARG  143

           所有这些字节码的操作含义在Python自带的文档中有专门的一页进行描述,当然,也可以到下面的网址察看:http://docs.python.org/lib/bytecodes.html。

    细心的你一定发现了,byte code的编码却到了143。没错,Python2.4.1中byte code的编码并没有按顺序增长,比如编码为5的ROT_FOUR之后就是编码为9的NOP。这可能是历史遗留下来的,你知道,在咱们这行,历史问题不是什么好东西,搞得现在还有许多人不得不很郁闷地面对MFC :)

    Python的143条byte code中,有一部分是需要参数的,另一部分是没有参数的。所有需要参数的byte code的编码都大于或等于90。Python中提供了专门的宏来判断一条byte code是否需要参数:

    [opcode.h]

    #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)

    好了,到了现在,关于PyCodeObject和pyc文件的一切我们都已了如指掌了,关于Python的现在我们可以做一些非常有趣的事了。呃,在我看来,最有趣的事莫过于自己写一个pyc文件的解析器。没错,利用我们现在所知道的一切,我们真的可以这么做了。图7展现的是对本章前面的那个test.py的解析结果:

    更进一步,我们还可以解析byte code。前面我们已经知道,Python在生成pyc文件时,会将PyCodeObject对象中的byte code也写入到pyc文件中,而且这个pyc文件中还记录了每一条byte code与Python源代码的对应关系,嗯,就是那个co_lnotab啦。假如现在我们知道了byte code在co_code中的偏移地址,那么与这条byte code对应的Python源代码的位置可以通过下面的算法得到(Python伪代码):

    lineno = addr = 0

    for addr_incr, line_incr in c_lnotab:

         addr += addr_incr

         if addr > A:

             return lineno

      lineno += line_incr

    下面是对一段Python源代码反编译为byte code的结果,这个结果也将作为下一章对Python执行引擎的分析的开始:

    i = 1

    #   LOAD_CONST   0

    #   STORE_NAME   0

    s = "Python"

    #   LOAD_CONST   1

    #   STORE_NAME   1

    d = {}

    #   BUILD_MAP   0

    #   STORE_NAME   2

    l = []

    #   BUILD_LIST   0

    #   STORE_NAME   3

    #   LOAD_CONST   2

    #   RETURN_VALUE   none

    再往前想一想,从现在到达的地方出发,实际上我们就可以做出一个Python的执行引擎了,哇,这是多么激动人心的事啊。遥远的天空,一抹朝阳,缓缓升起了……

    事实上,Python标准库中提供了对python进行反编译的工具dis,利用这个工具,可以很容易地得到我们在这里得到的结果,当然,还要更详细一些,图8展示了利用dis工具对CodeObject.py进行反编译的结果:

    在图8显示的结果中,最左面一列显示的是CodeObject.py中源代码的行数,左起第二列显示的是当前的字节码指令在co_code中的偏移位置。

    在以后的分析中,我们大部分将采用dis工具的反编译结果,在有些特殊情况下会使用我们自己的反编译结果。

    1.      PyCodeObject与Pyc文件通常认为,Python是一种解释性的语言,但是这种说法是不正确的,实际上,Python在执行时,首先会将.py文件中的源代码编译成Python的byte code(字节码),然后再由Python Virtual Machine来执行这些编译好的byte code。这种机制的基本思想跟Java,.NET是一致的。然而,Python Virtual Machine与Java或.NET的Virtual Machine不同的是,Python的Virtual Machine是一种更高级的Virtual Machine。这里的高级并不是通常意义上的高级,不是说Python的Virtual Machine比Java或.NET的功能更强大,更拽,而是说和Java或.NET相比,Python的Virtual Machine距离真实机器的距离更远。或者可以这么说,Python的Virtual Machine是一种抽象层次更高的Virtual Machine。
           我们来考虑下面的Python代码:
    [demo.py]
    class A:
        pass
     
    def Fun():
        pass
     
    value = 1
    str = “Python”
    a = A()
    Fun()
          
    Python在执行CodeObject.py时,首先需要进行的动作就是对其进行编译,编译的结果是什么呢?当然有字节码,否则Python也就没办法在玩下去了。然而除了字节码之外,还包含其它一些结果,这些结果也是Python运行的时候所必需的。看一下我们的demo.py,用我们的眼睛来解析一下,从这个文件中,我们可以看到,其中包含了一些字符串,一些常量值,还有一些操作。当然,Python对操作的处理结果就是自己码。那么Python的编译过程对字符串和常量值的处理结果是什么呢?实际上,这些在Python源代码中包含的静态的信息都会被Python收集起来,编译的结果中包含了字符串,常量值,字节码等等在源代码中出现的一切有用的静态信息。而这些信息最终会被存储在Python运行期的一个对象中,当Python运行结束后,这些信息甚至还会被存储在一种文件中。这个对象和文件就是我们这章探索的重点:PyCodeObject对象和Pyc文件。
    可以说,PyCodeObject就是Python源代码编译之后的关于程序的静态信息的集合:
    [compile.h]/* Bytecode object */ 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) */ } PyCodeObject;  
    在对Python源代码进行编译的时候,对于一段Code(Code Block),会创建一个PyCodeObject与这段Code对应。那么如何确定多少代码算是一个Code Block呢,事实上,当进入新的作用域时,就开始了新的一段Code。也就是说,对于下面的这一段Python源代码:
    [CodeObject.py]
    class A:
        pass
     
    def Fun():
        pass
     
    a = A()
    Fun()
     
    在Python编译完成后,一共会创建3个PyCodeObject对象,一个是对应CodeObject.py的,一个是对应class A这段Code(作用域),而最后一个是对应def Fun这段Code的。每一个PyCodeObject对象中都包含了每一个代码块经过编译后得到的byte code。但是不幸的是,Python在执行完这些byte code后,会销毁PyCodeObject,所以下次再次执行这个.py文件时,Python需要重新编译源代码,创建三个PyCodeObject,然后执行byte code。
    很不爽,对不对?Python应该提供一种机制,保存编译的中间结果,即byte code,或者更准确地说,保存PyCodeObject。事实上,Python确实提供了这样一种机制——Pyc文件。
    Python中的pyc文件正是保存PyCodeObject的关键所在,我们对Python解释器的分析就从pyc文件,从pyc文件的格式开始。
    在分析pyc的文件格式之前,我们先来看看如何产生pyc文件。在执行一个.py文件中的源代码之后,Python并不会自动生成与该.py文件对应的.pyc文件。我们需要自己触发Python来创建pyc文件。下面我们提供一种使Python创建pyc文件的方法,其实很简单,就是利用Python的import机制。
    在Python运行的过程中,如果碰到import abc,这样的语句,那么Python将到设定好的path中寻找abc.pyc或abc.dll文件,如果没有这些文件,而只是发现了abc.py,那么Python会首先将abc.py编译成相应的PyCodeObject的中间结果,然后创建abc.pyc文件,并将中间结果写入该文件。接下来,Python才会对abc.pyc文件进行一个import的动作,实际上也就是将abc.pyc文件中的PyCodeObject重新在内存中复制出来。了解了这个过程,我们很容易利用下面所示的generator.py来创建上面那段代码(CodeObjectt.py)对应的pyc文件了。
    generator.py
    CodeObject.py
    import test
    print "Done"
     
    class A:
    pass
     
    def Fun():
    pass
     
    a = A()
    Fun()
     
    图1所示的是Python产生的pyc文件:




    可以看到,pyc是一个二进制文件,那么Python如何解释这一堆看上去毫无意义的字节流就至关重要了。这也就是pyc文件的格式。
    要了解pyc文件的格式,首先我们必须要清楚PyCodeObject中每一个域都表示什么含义,这一点是无论如何不能绕过去的。
    Field
    Content
    co_argcount
    Code Block的参数的个数,比如说一个函数的参数
    co_nlocals
    Code Block中局部变量的个数
    co_stacksize
    执行该段Code Block需要的栈空间
    co_flags
    N/A
    co_code
    Code Block编译所得的byte code。以PyStringObject的形式存在
    co_consts
    PyTupleObject对象,保存该Block中的常量
    co_names
    PyTupleObject对象,保存该Block中的所有符号
    co_varnames
    N/A
    co_freevars
    N/A
    co_cellvars
    N/A
    co_filename
    Code Block所对应的.py文件的完整路径
    co_name
    Code Block的名字,通常是函数名或类名
    co_firstlineno
    Code Block在对应的.py文件中的起始行
    co_lnotab
    byte code与.py文件中source code行号的对应关系,以PyStringObject的形式存在
    需要说明一下的是co_lnotab域。在Python2.3以前,有一个byte code,唤做SET_LINENO,这个byte code会记录.py文件中source code的位置信息,这个信息对于调试和显示异常信息都有用。但是,从Python2.3之后,Python在编译时不会再产生这个byte code,相应的,Python在编译时,将这个信息记录到了co_lnotab中。
    co_lnotab中的byte code和source code的对应信息是以unsigned bytes的数组形式存在的,数组的形式可以看作(byte code在co_code中位置增量,代码行数增量)形式的一个list。比如对于下面的例子:
    Byte code在co_code中的偏移
    .py文件中源代码的行数
    0
    1
    6
    2
    50
    7
    这里有一个小小的技巧,Python不会直接记录这些信息,相反,它会记录这些信息间的增量值,所以,对应的co_lnotab就应该是:0,1, 6,1, 44,5。
    2.      Pyc文件的生成前面我们提到,Python在import时,如果没有找到相应的pyc文件或dll文件,就会在py文件的基础上自动创建pyc文件。那么,要想了解pyc的格式到底是什么样的,我们只需要考察Python在将编译得到的PyCodeObject写入到pyc文件中时到底进行了怎样的动作就可以了。下面的函数就是我们的切入点:
    [import.c]static void write_compiled_module(PyCodeObject *co, char *cpathname, long mtime) {     FILE *fp;     fp = open_exclusive(cpathname);     PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);          /* First write a 0 for mtime */     PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);     PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);          /* Now write the true mtime */     fseek(fp, 4L, 0);     PyMarshal_WriteLongToFile(mtime, fp, Py_MARSHAL_VERSION);     fflush(fp);     fclose(fp); }  
    这里的cpathname当然是pyc文件的绝对路径。首先我们看到会将pyc_magic这个值写入到文件的开头。实际上,pyc­_magic对应一个MAGIC的值。MAGIC是用来保证Python兼容性的一个措施。比如说要防止Python2.4的运行环境加载由Python1.5产生的pyc文件,那么只需要将Python2.4和Python1.5的MAGIC设为不同的值就可以了。Python在加载pyc文件时会首先检查这个MAGIC值,从而拒绝加载不兼容的pyc文件。那么pyc文件为什么会不兼容了,一个最主要的原因是byte code的变化,由于Python一直在不断地改进,有一些byte code退出了历史舞台,比如上面提到的SET_LINENO;或者由于一些新的语法特性会加入新的byte code,这些都会导致Python的不兼容问题。
    pyc文件的写入动作最后会集中到下面所示的几个函数中(这里假设代码只处理写入到文件,即p->fp是有效的。因此代码有删减,另有一个w_short未列出。缺失部分,请参考Python源代码):
    [marshal.c]typedef struct {     FILE *fp;     int error;     int depth;     PyObject *strings; /* dict on marshal, list on unmarshal */ } WFILE;    #define w_byte(c, p) putc((c), (p)->fp)    static void w_long(long x, WFILE *p) {     w_byte((char)( x      & 0xff), p);     w_byte((char)((x>> 8) & 0xff), p);     w_byte((char)((x>>16) & 0xff), p);     w_byte((char)((x>>24) & 0xff), p); }    static void w_string(char *s, int n, WFILE *p) {     fwrite(s, 1, n, p->fp); }  
    在调用PyMarshal_WriteLongToFile时,会直接调用w_long,但是在调用PyMarshal_WriteObjectToFile时,还会通过一个间接的函数:w_object。需要特别注意的是PyMarshal_WriteObjectToFile的第一个参数,这个参数正是Python编译出来的PyCodeObject对象。
    w_object的代码非常长,这里就不全部列出。其实w_object的逻辑非常简单,就是对应不同的对象,比如string,int,list等,会有不同的写的动作,然而其最终目的都是通过最基本的w_long或w_string将整个PyCodeObject写入到pyc文件中。
    对于PyCodeObject,很显然,会遍历PyCodeObject中的所有域,将这些域依次写入:
    [marshal.c]static void w_object(PyObject *v, WFILE *p) {    ……     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); } …… } 
    而对于一个PyListObject对象,想象一下会有什么动作?没错,还是遍历!!!:
    [w_object() in marshal.c]…… else if (PyList_Check(v))     {         w_byte(TYPE_LIST, p);         n = PyList_GET_SIZE(v);         w_long((long)n, p);         for (i = 0; i < n; i++)         {             w_object(PyList_GET_ITEM(v, i), p);         } }……  
    而如果是PyIntObject,嗯,那太简单了,几乎没有什么可说的:
    [w_object() in marshal.c] ……
    else if (PyInt_Check(v))     {         w_byte(TYPE_INT, p);         w_long(x, p);     } ……
     
    有没有注意到TYPE_LIST,TYPE_CODE,TYPE_INT这样的标志?pyc文件正是利用这些标志来表示一个新的对象的开始,当加载pyc文件时,加载器才能知道在什么时候应该进行什么样的加载动作。这些标志同样也是在import.c中定义的:
    [import.c]#define TYPE_NULL   '0' #define TYPE_NONE   'N'。。。。。。 #define TYPE_INT    'i' #define TYPE_STRING 's' #define TYPE_INTERNED   't' #define TYPE_STRINGREF  'R' #define TYPE_TUPLE  '(' #define TYPE_LIST   '[' #define TYPE_CODE   'c'  
    到了这里,可以看到,Python对于中间结果的导出实际是不复杂的。实际上在write的动作中,不论面临PyCodeObject还是PyListObject这些复杂对象,最后都会归结为简单的两种形式,一个是对数值的写入,一个是对字符串的写入。上面其实我们已经看到了对数值的写入过程。在写入字符串时,有一套比较复杂的机制。在了解字符串的写入机制前,我们首先需要了解一个写入过程中关键的结构体WFILE(有删节):
    [marshal.c]typedef struct {     FILE *fp;     int error;     int depth;     PyObject *strings; /* dict on marshal, list on unmarshal */ } WFILE;  
    这里我们也只考虑fp有效,即写入到文件,的情况。WFILE可以看作是一个对FILE*的简单包装,但是在WFILE里,出现了一个奇特的strings域。这个域是在pyc文件中写入或读出字符串的关键所在,当向pyc中写入时,string会是一个PyDictObject对象;而从pyc中读出时,string则会是一个PyListObject对象。
    [marshal.c]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;    w_object(x, &wf); }  
    可以看到,strings在真正开始写入之前,就已经被创建了。在w_object中对于字符串的处理部分,我们可以看到对strings的使用:
    [w_object() in marshal.c] ……
    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             {                 o = PyInt_FromLong(PyDict_Size(p->strings));                 PyDict_SetItem(p->strings, v, o);                 Py_DECREF(o);                 w_byte(TYPE_INTERNED, p);             }         }         else         {             w_byte(TYPE_STRING, p);         }         n = PyString_GET_SIZE(v);         w_long((long)n, p);         w_string(PyString_AS_STRING(v), n, p); }……
     
    真正有趣的事发生在这个字符串是一个需要被进行INTERN操作的字符串时。可以看到,WFILE的strings域实际上是一个从string映射到int的一个PyDictObject对象。这个int值是什么呢,这个int值是表示对应的string是第几个被加入到WFILE.strings中的字符串。
    这个int值看上去似乎没有必要,记录一个string被加入到WFILE.strings中的序号有什么意义呢?好,让我们来考虑下面的情形:
    假设我们需要向pyc文件中写入三个string:”Jython”, “Ruby”, “Jython”,而且这三个string都需要被进行INTERN操作。对于前两个string,没有任何问题,闭着眼睛写入就是了。完成了前两个string的写入后,WFILE.strings与pyc文件的情况如图2所示:
      

    在写入第三个字符串的时候,麻烦来了。对于这个“Jython”,我们应该怎么处理呢?是按照上两个string一样吗?如果这样的话,那么写入后,WFILE.strings和pyc的情况如图3所示:
    我们可以不管WFILE.strings怎么样了,但是一看pyc文件,我们就知道,问题来了。在pyc文件中,出现了重复的内容,关于“Jython”的信息重复了两次,这会引起什么麻烦呢?想象一下在python代码中,我们创建了一个button,在此之后,多次使用了button,这样,在代码中,“button”将出现多次。想象一下吧,我们的pyc文件会变得多么臃肿,而其中充斥的只是毫无价值的冗余信息。如果你是Guido,你能忍受这样的设计吗?当然不能!!于是Guido给了我们TYPE_STRINGREF这个东西。在解析pyc文件时,这个标志表明后面的一个数值表示了一个索引值,根据这个索引值到WFILE.strings中去查找,就能找到需要的string了。
    有了TYPE_STRINGREF,我们的pyc文件就能变得苗条了,如图4所示:



    看一下加载pyc文件的过程,我们就能对这个机制更加地明了了。前面我们提到,在读入pyc文件时,WFILE.strings是一个PyListObject对象,所以在读入前两个字符串后,WFILE.strings的情形如图5所示:


    在加载紧接着的(R,0)时,因为解析到是一个TYPE_STRINGREF标志,所以直接以标志后面的数值0位索引访问WFILE.strings,立刻可得到字符串“Jython”。
    3.      一个PyCodeObject,多个PyCodeObject?到了这里,关于PyCodeObject与pyc文件,我们只剩下最后一个有趣的话题了。还记得前面那个test.py吗?我们说那段简单的什么都做不了的python代码就要产生三个PyCodeObject。而在write_compiled_module中我们又亲眼看到,Python运行环境只会对一个PyCodeObject对象调用PyMarshal_WriteObjectToFile操作。刹那间,我们竟然看到了两个遗失的PyCodeObject对象。
    Python显然不会犯这样低级的错误,想象一下,如果你是Guido,这个问题该如何解决?首先我们会假想,有两个PyCodeObject对象一定是包含在另一个PyCodeObject中的。没错,确实如此,还记得我们最开始指出的Python是如何确定一个Code Block的吗?对喽,就是作用域。仔细看一下test.py,你会发现作用域呈现出一种嵌套的结构,这种结构也正是PyCodeObject对象之间的结构。所以到现在清楚了,与Fun和A对应得PyCodeObject对象一定是包含在与全局作用域对应的PyCodeObject对象中的,而PyCodeObject结构中的co_consts域正是这两个PyCodeObject对象的藏身之处,如图6所示:


    在对一个PyCodeObject对象进行写入到pyc文件的操作时,如果碰到它包含的另一个PyCodeObject对象,那么就会递归地执行写入PyCodeObject对象的操作。如此下去,最终所有的PyCodeObject对象都会被写入到pyc文件中去。而且pyc文件中的PyCodeObject对象也是以一种嵌套的关系联系在一起的。
    4.      Python字节码Python源代码在执行前会被编译为Python的byte code,Python的执行引擎就是根据这些byte code来进行一系列的操作,从而完成对Python程序的执行。在Python2.4.1中,一共定义了103条byte code:
    [opcode.h]
    #define STOP_CODE   0
    #define POP_TOP     1
    #define ROT_TWO     2
    ……
    #define CALL_FUNCTION_KW           141
    #define CALL_FUNCTION_VAR_KW       142
    #define EXTENDED_ARG  143
     
           所有这些字节码的操作含义在Python自带的文档中有专门的一页进行描述,当然,也可以到下面的网址察看:http://docs.python.org/lib/bytecodes.html。
    细心的你一定发现了,byte code的编码却到了143。没错,Python2.4.1中byte code的编码并没有按顺序增长,比如编码为5的ROT_FOUR之后就是编码为9的NOP。这可能是历史遗留下来的,你知道,在咱们这行,历史问题不是什么好东西,搞得现在还有许多人不得不很郁闷地面对MFC :)
    Python的143条byte code中,有一部分是需要参数的,另一部分是没有参数的。所有需要参数的byte code的编码都大于或等于90。Python中提供了专门的宏来判断一条byte code是否需要参数:
    [opcode.h]
    #define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)
     
    好了,到了现在,关于PyCodeObject和pyc文件的一切我们都已了如指掌了,关于Python的现在我们可以做一些非常有趣的事了。呃,在我看来,最有趣的事莫过于自己写一个pyc文件的解析器。没错,利用我们现在所知道的一切,我们真的可以这么做了。图7展现的是对本章前面的那个test.py的解析结果:


     
     
    更进一步,我们还可以解析byte code。前面我们已经知道,Python在生成pyc文件时,会将PyCodeObject对象中的byte code也写入到pyc文件中,而且这个pyc文件中还记录了每一条byte code与Python源代码的对应关系,嗯,就是那个co_lnotab啦。假如现在我们知道了byte code在co_code中的偏移地址,那么与这条byte code对应的Python源代码的位置可以通过下面的算法得到(Python伪代码):
    lineno = addr = 0
    for addr_incr, line_incr in c_lnotab:
         addr += addr_incr
         if addr > A:
             return lineno
      lineno += line_incr
     
    下面是对一段Python源代码反编译为byte code的结果,这个结果也将作为下一章对Python执行引擎的分析的开始:
    i = 1
    #   LOAD_CONST   0
    #   STORE_NAME   0
     
    s = "Python"
    #   LOAD_CONST   1
    #   STORE_NAME   1
     
    d = {}
    #   BUILD_MAP   0
    #   STORE_NAME   2
     
    l = []
    #   BUILD_LIST   0
    #   STORE_NAME   3
    #   LOAD_CONST   2
    #   RETURN_VALUE   none
     
    再往前想一想,从现在到达的地方出发,实际上我们就可以做出一个Python的执行引擎了,哇,这是多么激动人心的事啊。遥远的天空,一抹朝阳,缓缓升起了……
    事实上,Python标准库中提供了对python进行反编译的工具dis,利用这个工具,可以很容易地得到我们在这里得到的结果,当然,还要更详细一些,图8展示了利用dis工具对CodeObject.py进行反编译的结果:



    在图8显示的结果中,最左面一列显示的是CodeObject.py中源代码的行数,左起第二列显示的是当前的字节码指令在co_code中的偏移位置。
    在以后的分析中,我们大部分将采用dis工具的反编译结果,在有些特殊情况下会使用我们自己的反编译结果。

  • 相关阅读:
    「枫桥夜泊」一诗
    走遍亚洲 —— 泰国
    走遍亚洲 —— 泰国
    暴露年龄
    暴露年龄
    插入排序(insertion sort)
    开机黑屏 仅仅显示鼠标 电脑黑屏 仅仅有鼠标 移动 [已成功解决]
    OpenCV For iOS 1:&#160;连接OpenCV 3.0
    插入排序
    [hadoop系列]Pig的安装和简单演示样例
  • 原文地址:https://www.cnblogs.com/rsapaper/p/9811429.html
Copyright © 2011-2022 走看看