浏览 17166 次
锁定老帖子 主题:Python 2.6.2的.pyc文件格式
精华帖 (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与校验 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2009-05-09
强了……这也能够看……- -+
|
|
返回顶楼 | |