Python源码剖析--Pyc文件解析

1.      PyCodeObjectPyc文件

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

       咱们来考虑下面的Python代码:python

[demo.py]算法

class A:数组

    passapp

 

def Fun():函数

    pass工具

 

value = 1ui

str = “Python”编码

a = A()lua

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源代码进行编译的时候,对于一段CodeCode Block),会建立一个PyCodeObject与这段Code对应。那么如何肯定多少代码算是一个Code Block呢,事实上,当进入新的做用域时,就开始了新的一段Code。也就是说,对于下面的这一段Python源代码:

[CodeObject.py]

class A:

    pass

 

def Fun():

    pass

 

a = A()

Fun()

 

Python编译完成后,一共会建立3PyCodeObject对象,一个是对应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文件的方法,其实很简单,就是利用Pythonimport机制。

Python运行的过程当中,若是碰到import abc,这样的语句,那么Python将到设定好的path中寻找abc.pycabc.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 codesource code的对应信息是以unsigned bytes的数组形式存在的,数组的形式能够看做(byte codeco_code中位置增量,代码行数增量)形式的一个list。好比对于下面的例子:

Byte codeco_code中的偏移

.py文件中源代码的行数

0

1

6

2

50

7

这里有一个小小的技巧,Python不会直接记录这些信息,相反,它会记录这些信息间的增量值,因此,对应的co_lnotab就应该是01 61 445

2.      Pyc文件的生成

前面咱们提到,Pythonimport时,若是没有找到相应的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.4Python1.5MAGIC设为不一样的值就能够了。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的逻辑很是简单,就是对应不一样的对象,好比stringintlist等,会有不一样的写的动做,然而其最终目的都是经过最基本的w_longw_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_LISTTYPE_CODETYPE_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操做的字符串时。能够看到,WFILEstrings域其实是一个从string映射到int的一个PyDictObject对象。这个int值是什么呢,这个int值是表示对应的string是第几个被加入到WFILE.strings中的字符串。

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

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

 

在写入第三个字符串的时候,麻烦来了。对于这个“Jython”,咱们应该怎么处理呢?
是按照上两个string同样吗?若是这样的话,那么写入后,WFILE.stringspyc的状况如图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所示:

在加载紧接着的(R0)时,由于解析到是一个TYPE_STRINGREF标志,因此直接以标志后面的数值0位索引访问WFILE.strings,马上可获得字符串“Jython”。

3.      一个PyCodeObject,多个PyCodeObject

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

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

在对一个PyCodeObject对象进行写入到pyc文件的操做时,若是碰到它包含的另外一个PyCodeObject对象,那么就会递归地执行写入PyCodeObject对象的操做。如此下去,最终全部的PyCodeObject对象都会被写入到pyc文件中去。并且pyc文件中的PyCodeObject对象也是以一种嵌套的关系联系在一块儿的。

4.      Python字节码

Python源代码在执行前会被编译为Pythonbyte codePython的执行引擎就是根据这些byte code来进行一系列的操做,从而完成对Python程序的执行。在Python2.4.1中,一共定义了103byte 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.1byte code的编码并无按顺序增加,好比编码为5ROT_FOUR以后就是编码为9NOP。这多是历史遗留下来的,你知道,在我们这行,历史问题不是什么好东西,搞得如今还有许多人不得不很郁闷地面对MFC :)

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

[opcode.h]

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

 

好了,到了如今,关于PyCodeObjectpyc文件的一切咱们都已了如指掌了,关于Python的如今咱们能够作一些很是有趣的事了。呃,在我看来,最有趣的事莫过于本身写一个pyc文件的解析器。没错,利用咱们如今所知道的一切,咱们真的能够这么作了。图7展示的是对本章前面的那个test.py的解析结果:

 

 

更进一步,咱们还能够解析byte code。前面咱们已经知道,Python在生成pyc文件时,会将PyCodeObject对象中的byte code也写入到pyc文件中,并且这个pyc文件中还记录了每一条byte codePython源代码的对应关系,嗯,就是那个co_lnotab啦。假如如今咱们知道了byte codeco_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工具的反编译结果,在有些特殊状况下会使用咱们本身的反编译结果。

相关文章
相关标签/搜索