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) 和Java或.NET相比,Python的Virtual Machine距离真实机器的距离更远的更多相关文章

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

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

  2. py, pyc, pyw, pyo, pyd 及发布程序时的选择 Compiled Python File (.pyc)

    Python 程序扩展名(py, pyc, pyw, pyo, pyd)及发布程序时的选择 - 司开星的专栏 - CSDN博客 https://blog.csdn.net/chroming/artic ...

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

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

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

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

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

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

  6. python介绍、安装及相关语法、python运维、编译与解释

    1.python介绍 Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/)是一种广泛使用的解释型.高级编程.通用型编程语言,由吉多.范罗苏姆创造,第一版发布于1991年.可以视 ...

  7. Virtual Machine Definition File 2.2

    Virtual Machine Definition File 2.2 http://archives.opennebula.org/documentation:archives:rel2.2:tem ...

  8. 2019年Python、Golang、Java、C++如何选择?

    前言 作为开发者我们都知道,开发后台语言可选择的方向会很多,比如,Java,go,Python,C/C++,PHP,NodeJs….等很多,这么多语言都有什么样的优势?如果让你学习一门后端语言,又该如 ...

  9. Python是解释性语言吗? 直到看到有 python py、pyc、pyo、pyd 文件

    py是源文件,pyc是源文件编译后的文件,pyo是源文件优化编译后的文件,pyd是其他语言写的python库 1. Python是一门解释型语言? Python是一门解释性语言,我就这样一直相信下去, ...

随机推荐

  1. 使用ajax请求上传多个或者多个附件

    jsp页面 <%@ page language="java" pageEncoding="UTF-8"%> <!DOCTYPE HTML> ...

  2. 使用 dotnet-monitor 分析.NET 应用程序

    dotnet-monitor 是 .NET Core 命令行接口 (CLI) 工具, 可以很方便的在dotnet环境中分析我们的应用程序,需要注意的是,目前它还只是一个实验性的工具 在这之前,我们使用 ...

  3. linux学习之--虚拟机安装linux【centerOS】

    计划把学习中的软件安装使用记录下来,以下是使用VMware 按照 Linux 使用桥接网络虚拟机和windows中都有不同的ip地址

  4. Access-Control-Allow-Headers等基础常识

    跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种机制,该机制使用附加的 HTTP 头来告诉浏览器,准许运行在一个源上的Web应用访问位于另一不同源选定的资源. 当一个Web应用发起一个与 ...

  5. java数组之binarySearch查找

    /** * 1.如果找到目标对象则返回<code>[公式:-插入点-1]</code> * 插入点:第一个大与查找对象的元素在数组中的位置,如果数组中的所有元素都小于要查找的对 ...

  6. 使用mono-repo实现跨项目组件共享

    本文会分享一个我在实际工作中遇到的案例,从最开始的需求分析到项目搭建,以及最后落地的架构的整个过程.最终实现的效果是使用mono-repo实现了跨项目的组件共享.在本文中你可以看到: 从接到需求到深入 ...

  7. Alpha冲刺——汇总博客

    一.代码规范与计划 代码规范与计划 二.10篇冲刺随笔 冲刺随笔--Day1 冲刺随笔--Day2 冲刺随笔--Day3 冲刺随笔--Day4 冲刺随笔--Day5 冲刺随笔--Day6 冲刺随笔-- ...

  8. MAC与ARP缓存中毒介绍

    ARP 协议 用于地址解析,请求MAC地址. arp -a 或者 -n 查看ARP缓存表 ls(ARP) 查看scapy里的协议字段 ARP缓存中毒原理 ARP收到ARP请求报文,会将发送方的mac地 ...

  9. 树莓派(4B)新手入门教程

    前期准备 必要物料 树莓派4B 主机 Type-C 电源 内存卡(8G+) 一般建议一步到位64G 系统镜像 镜像写入工具 下载地址 镜像下载 官方下载地址: https://www.raspberr ...

  10. 【函数分享】每日PHP函数分享(2021-1-11)

    str_shuffle() 随机打乱一个字符串. string str_shuffle ( string $str ) 参数描述 str     输入字符串.返回值:返回打乱后的字符串.实例: < ...