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工具的反编译结果,在有些特殊情况下会使用我们自己的反编译结果。

py, pyc, pyw, pyo, pyd 及发布程序时的选择 Compiled Python File (.pyc)的更多相关文章

  1. Python文件格式 .py .pyc .pyw .pyo .pyd的主要区别

    Python是一种面向对象.解释型计算机程序设计语言.Python 语法简洁.清晰,具有丰富和强大的类库.Python源代码遵循 GPL (GNU General Public License) 协议 ...

  2. python .py .pyc .pyw .pyo .pyd区别

    .py 文件 以 .py 作扩展名的文件是 Python 源代码文件,由 python.exe 解释,可在控制台下运行.当然,也可用文本编辑器进行修改. .pyc 文件 以 .pyc 作扩展名的文件是 ...

  3. Python各种扩展名(py, pyc, pyw, pyo, pyd)区别

    扩展名 在写Python程序时我们常见的扩展名是py, pyc,其实还有其他几种扩展名.下面是几种扩展名的用法. py py就是最基本的源码扩展名 pyw pyw是另一种源码扩展名,跟py唯一的区别是 ...

  4. 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是一种解释性的语言,但是这种说法 ...

  5. python py pyc pyw pyo pyd之间区别

    来源: http://blog.csdn.net/chroming/article/details/52083387 1.py 脚本文件,是最基本的源码扩展名.windows下直接双击运行会调用pyt ...

  6. Unity3D发布WebPlayer时Failed to download data file解决方案

    今天发布WebPlayer时, 发现直接打开html是可以正常运行的, 但是通过iis访问的话就会报错: Failed to download data file. 一开始以为是防火墙, 后来发现不是 ...

  7. .net 发布程序时出现“类型ASP.global_asax同时存在于...”错误的解决办法

    web程序发布后,通过浏览器访问程序显示如下的错误信息: 编译器错误消息: CS0433: 类型“ASP.global_asax”同时存在于“c:\WINDOWS\Microsoft.NET\Fram ...

  8. 发布程序时出现“类型ASP.global_asax同时存在于...”错误的解决办法

    web程序发布后,通过浏览器访问程序显示如下的错误信息: 编译器错误消息: CS0433: 类型“ASP.global_asax”同时存在于“c:\WINDOWS\Microsoft.NET\Fram ...

  9. Py层次递进与文件修改大程序,模块,name与file

    层次的递进与返回 #输入quit的时候返回上一阶层,输入exit退出所有的循环 tag=True while tag==True: level1=input('level1:') if level1= ...

随机推荐

  1. AxureRP_for_chorme的安装和使用方法

    1.下载AxureRP_for_chorme_version.crx 2.打开Chrome,右上角菜单图标->更多->扩展程序 3.将crx文件拖入,安装 4.选中AxureRP的“已启用 ...

  2. Mac上搭建直播服务器Nginx

    Mac上搭建直播服务器Nginx 1.安装Homebrew,执行命令 Homebrew简称brew,是Mac OSX上的软件包管理工具,能在Mac中方便的安装软件或者卸载软件,可以说Homebrew就 ...

  3. sitemesh 学习之 meta 引入

    在上篇笔记学习了sitemesh的基本用法,这里还有另一种用法 在sitemesh.jar有一个默认的sitemesh-default文件 ,这个文件是可以指定的 可以指定的文件名的sitemesh. ...

  4. 语言中.C文件和.H文件的概念和联系

    //a.h void foo(); //a.c #include "a.h" //我的问题出来了:这句话是要,还是不要? void foo() { return; } //main ...

  5. Vim 学习笔记二

    1. 粘帖 p 光标前 P 2. 撤销对撤销的撤销 Ctrl+r 3. dl:删除一个字符,daw:删除一个单词,dap:删除一个段落 4. 单个c字符并无效果,cc删除整个一行 C:从当前光标出删除 ...

  6. git版本管理之git-ssh 配置和使用

    1.设置用户名和邮箱 $ git config --global user.name "gsx-gh" $ git config --global user.email " ...

  7. 第二百四十二节,Bootstrap列表组面板和嵌入组件

    Bootstrap列表组面板和嵌入组件 学习要点: 1.列表组组件 2.面板组件 3.响应式嵌入组件 本节课我们主要学习一下 Bootstrap 的三个组件功能:列表组组件.面板组件. 响应 式嵌入组 ...

  8. struts2中,OGNL访问值栈的时候查找的顺序是什么?请排序:模型对象、临时对象、固定名称的对象、Action对象

    struts2中,OGNL访问值栈的时候查找的顺序是什么?请排序:模型对象.临时对象.固定名称的对象.Action对象 解答:struts2的值栈排列顺序为:1).临时对象:2).模型对象:3).Ac ...

  9. (转)负载均衡,回话保持,cookie

    servlet操作cookie:http://elf8848.iteye.com/blog/253198 负载均衡,回话保持:http://www.cnblogs.com/qq78292959/arc ...

  10. 该如何将MathType公式粘贴到文档中

    MathType是一款非常好用的数学公式编辑器,因为它与很多的文档处理器都能够很好地兼容,因此其使用范围非常的大.MathType公式编辑器编辑的公式与数学符号等都符号国际期刊的出版标准,并且上手容易 ...