论坛首页 编程语言技术论坛

Python 2.6.2的.pyc文件格式

浏览 17187 次
精华帖 (1) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-05-09   最后修改:2009-05-09
相关链接:
以Python为例讨论高级编程语言程序的wire format与校验
Python 2.6.2的字节码指令集一览

.pyc文件是什么?

Python源码编译的结果就是PyCodeObject(下面将PyCodeObject的实例简称为“代码对象”),每个作用域会编译出一个对应的代码对象,其中名为co_code的PyStringObject保存着代码对象的字节码。
一个Python源文件就是一个模块。每个模块顶层的代码对象通过marshal序列化之后就得到了.pyc文件。marshal以little-endian字节序来序列化数据。
那嵌套于顶层作用域里面的那些作用域,例如函数、类的定义,它们对应的代码对象在哪里?它们每一个都乖乖的躺在上一层作用域的代码对象的co_const(常量池)域里,所以其实顶层代码对象已经嵌套包含了底下其它作用域的代码对象。
PyCodeObject的结构和marshal的序列化逻辑请参考上一段的两个链接。

Python 2.6.2的.pyc文件的结构

============== <- 文件起始。下面是文件头信息
pyc_magic     (=0xD1 0xF2 0x0D 0x0A,4字节,简单校验.pyc的魔数)
mtime         (4字节,.pyc对应的源文件的修改时间)
============== <- 顶层PyCodeObject起始。下面都属于顶层代码对象
TYPE_CODE     (='c',1字节,PyCodeObject的类型标识)
co_argscount  (4字节,位置参数的个数)
co_nlocals    (4字节,局部变量(包括位置参数)的个数)
co_stacksize  (4字节,求值栈的最大深度)
co_flags      (4字节,意义不明)
co_code       (PyStringObject,字节码)
co_consts     (PyTupleObject,常量池)
co_names      (PyTupleObject,所有用到的符号的集合)
co_varnames   (PyTupleObject,局部变量名集合)
co_freevars   (PyTupleObject,自由变量的变量名集合)
co_cellvars   (PyTupleObject,被闭包捕获的局部变量的变量名集合)
co_filename   (PyStringObject,源文件名)
co_firstlineno(4字节,该代码对象中源码的首行对应行号)
co_lnotab     (PyStringObject,字节码偏移量与源码行号的对应关系)
============== <- 顶层PyCodeObject结束。文件结束

以上,整型的域都标出了长度,其它域则标出了类型。为了能清楚的看到文件头与后面的PyCodeObject的界线,用横线特别标注了出来。
因为关键的字节码是存在PyStringObject类型的对象里,该类型通过marshal序列化后的结构也有必要说明:
=========== <- PyStringObject起始
TYPE_STRING(='s',1字节,PyStringObject的类型标识)
length     (4字节,字符串内容的长度)
data       (byte数组,字符串内容)
=========== <- PyStringObject结束

了解这两个结构后,我们可以推出:在Python 2.6.2的.pyc文件中,地址在0x1A开始的4字节整型标识顶层代码对象的字节码的长度,假设长度为len;从0x1E开始,长度为len的数据就是顶层代码对象的字节码。

关于pyc_magic

pyc_magic是两个字节的整数,加上\r\n(0x0D 0x0A就是\r\n)。它是这样算出来的:
Python 2.6a1的Python/import.c
#define MAGIC (62161 | ((long)'\r'<<16) | ((long)'\n'<<24))

(注意marshal写出时保持little-endian字节序,所以\r\n就跑到后面去了。)
据说在magic里包含这样的数据是因为\r\n是换行符,如果.pyc文件被以文本模式打开并编辑,magic就会乱掉,那么Python解释器在试图加载这个损坏了的.pyc文件时就会发现有问题。
可是那个62161是怎么来的?通过阅读Include/code.h的注释,可以观察到每个主要的Python发行版本都有一个特别的数字,版本间各不相同。
于是pyc_magic的作用有三:
1、拒绝完全不可能是正常的.pyc的文件,例如普通文本,图片、音乐,或者别的二进制格式。检查文件的头4个字节已经能有效的筛掉许多无效文件。
2、拒绝不慎被文本编辑器编辑而破损的文件。
3、拒绝不对应的Python解释器生成的.pyc文件。由于不同Python版本的marshal算法可能不同,虚拟机采用的字节码指令集也可能不同,所以保守起见不同版本的Python解释器生成的.pyc文件被认为是不兼容的。

关于mtime

PyCodeObject被序列化为.pyc文件时,会连带对应源文件的修改时间一起记录下来。如果对应的源文件发生了修改,则其mtime会发生变化,以前生成的.pyc文件中的mtime就与源文件新的mtime不一致了。这样Python解释器就能够发现源文件有更新,并为其重新生成.pyc文件。

Python解释器生成.pyc文件一般是靠import机制来激活的,阅读Python/import.c可以看到import时对pyc_magic和mtime的检查和写入。

昨晚做了份Python 2.6.2的字节码指令集一览,有兴趣的同学可以看看~

实际解读一个.pyc文件的字节码
例如这样的一个Python源码:(随手写的,别介意内容)
demo.py
class A():
  x = 1

print(A.x) # 1

# increment A.x by 2
A.x += 2
print(A.x) # 3

# create an instance of A
a = A()
print(a.x) # 3

# increment A.x by 4
a.x += 4
print(a.x) # 7
print(A.x) # 3

让Python解释器将其编译为demo.pyc。只要随便在别的Python程序里import demo就足以得到这个文件了。文件内容是:
Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

00000000   D1 F2 0D 0A B3 34 04 4A  63 00 00 00 00 00 00 00   羊..?.Jc.......
00000010   00 03 00 00 00 40 00 00  00 73 66 00 00 00 64 00   .....@...sf...d.
00000020   00 64 05 00 64 01 00 84  00 00 83 00 00 59 5A 00   .d..d..?.?.YZ.
00000030   00 65 00 00 69 01 00 47  48 65 00 00 04 69 01 00   .e..i..GHe...i..
00000040   64 02 00 37 02 5F 01 00  65 00 00 69 01 00 47 48   d..7._..e..i..GH
00000050   65 00 00 83 00 00 5A 02  00 65 02 00 69 01 00 47   e..?.Z..e..i..G
00000060   48 65 02 00 04 69 01 00  64 03 00 37 02 5F 01 00   He...i..d..7._..
00000070   65 02 00 69 01 00 47 48  65 00 00 69 01 00 47 48   e..i..GHe..i..GH
00000080   64 04 00 53 28 06 00 00  00 74 01 00 00 00 41 63   d..S(....t....Ac
00000090   00 00 00 00 00 00 00 00  01 00 00 00 42 00 00 00   ............B...
000000A0   73 0E 00 00 00 65 00 00  5A 01 00 64 00 00 5A 02   s....e..Z..d..Z.
000000B0   00 52 53 28 01 00 00 00  69 01 00 00 00 28 03 00   .RS(....i....(..
000000C0   00 00 74 08 00 00 00 5F  5F 6E 61 6D 65 5F 5F 74   ..t....__name__t
000000D0   0A 00 00 00 5F 5F 6D 6F  64 75 6C 65 5F 5F 74 01   ....__module__t.
000000E0   00 00 00 78 28 00 00 00  00 28 00 00 00 00 28 00   ...x(....(....(.
000000F0   00 00 00 73 07 00 00 00  64 65 6D 6F 2E 70 79 52   ...s....demo.pyR
00000100   00 00 00 00 01 00 00 00  73 02 00 00 00 06 01 69   ........s......i
00000110   02 00 00 00 69 04 00 00  00 4E 28 00 00 00 00 28   ....i....N(....(
00000120   03 00 00 00 52 00 00 00  00 52 03 00 00 00 74 01   ....R....R....t.
00000130   00 00 00 61 28 00 00 00  00 28 00 00 00 00 28 00   ...a(....(....(.
00000140   00 00 00 73 07 00 00 00  64 65 6D 6F 2E 70 79 74   ...s....demo.pyt
00000150   08 00 00 00 3C 6D 6F 64  75 6C 65 3E 01 00 00 00   ....<module>....
00000160   73 10 00 00 00 13 03 08  03 0F 01 08 03 09 01 08   s...............
00000170   03 0F 01 08 01                                     .....

根据本文前半部分的分析,顶层代码的字节码长度是0x66=102字节,位于0x1E-0x83这个范围内。有兴趣的同学可以参考Python指令集列表来试试自行解读字节码指令。

看到字节码的开头,首先是0x64,这是一个LOAD_CONST指令,有参数,所以接着看后面两个字节,0x00 0x00,也就是参数为0(注意little-endian字节序)。LOAD_CONST 0,也就是把常量池下标为0的对象压到求值栈上。
接下来又是0x64,连上参数的0x05 0x00,就是LOAD_CONST 5,也就是把常量池下标为5的对象压到求值栈上。
后面的字节码也可依法类推辨认出来。

事实上Python标准库里已经有提取字节码的库,dis模块。自己分析嫌麻烦的话让dis帮忙解决就行:
(下面代码中,dis.dis(code)的结果就是顶层代码对象中的字节码,
格式:行号  偏移量 指令名 参数(括号内为参数的注解))
E:\Python26>python
Python 2.6.2 (r262:71605, Apr 14 2009, 22:40:02) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis, marshal
>>> f = open('demo.pyc', 'rb')
>>> magic = f.read(4)
>>> mtime = f.read(4)
>>> code = marshal.load(f)
>>> dis.dis(code)
  1           0 LOAD_CONST               0 ('A')
              3 LOAD_CONST               5 (())
              6 LOAD_CONST               1 (<code object A at 01707EC0, file "demo.py", line 1>)
              9 MAKE_FUNCTION            0
             12 CALL_FUNCTION            0
             15 BUILD_CLASS
             16 STORE_NAME               0 (A)

  4          19 LOAD_NAME                0 (A)
             22 LOAD_ATTR                1 (x)
             25 PRINT_ITEM
             26 PRINT_NEWLINE

  7          27 LOAD_NAME                0 (A)
             30 DUP_TOP
             31 LOAD_ATTR                1 (x)
             34 LOAD_CONST               2 (2)
             37 INPLACE_ADD
             38 ROT_TWO
             39 STORE_ATTR               1 (x)

  8          42 LOAD_NAME                0 (A)
             45 LOAD_ATTR                1 (x)
             48 PRINT_ITEM
             49 PRINT_NEWLINE

 11          50 LOAD_NAME                0 (A)
             53 CALL_FUNCTION            0
             56 STORE_NAME               2 (a)

 12          59 LOAD_NAME                2 (a)
             62 LOAD_ATTR                1 (x)
             65 PRINT_ITEM
             66 PRINT_NEWLINE

 15          67 LOAD_NAME                2 (a)
             70 DUP_TOP
             71 LOAD_ATTR                1 (x)
             74 LOAD_CONST               3 (4)
             77 INPLACE_ADD
             78 ROT_TWO
             79 STORE_ATTR               1 (x)

 16          82 LOAD_NAME                2 (a)
             85 LOAD_ATTR                1 (x)
             88 PRINT_ITEM
             89 PRINT_NEWLINE

 17          90 LOAD_NAME                0 (A)
             93 LOAD_ATTR                1 (x)
             96 PRINT_ITEM
             97 PRINT_NEWLINE
             98 LOAD_CONST               4 (None)
            101 RETURN_VALUE
>>> quit()


P.S. 其实这文单独看挺火星的……我是为了让另外一帖更干净些才把分析.pyc文件格式的部分拆了出来。另外一帖是:以Python为例讨论高级编程语言程序的wire format与校验
   发表时间:2009-05-09  
强了……这也能够看……- -+
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics