這篇文章只是純粹分析python pyc文件格式胜茧,主要是關(guān)于pyc在文件中的存儲(chǔ)方式進(jìn)行了解析脑题。pyc是python字節(jié)碼在文件中存儲(chǔ)的方式摇展,而在虛擬機(jī)運(yùn)行時(shí)環(huán)境中對(duì)應(yīng)PyCodeObject對(duì)象洋闽。關(guān)于PyFrameObject以及PyFunctionObject等運(yùn)行時(shí)結(jié)構(gòu)阐污,后續(xù)希望學(xué)習(xí)透徹了能夠一并分析缰猴。
1.示例文件
源文件test.py
s = "hello"
def func():
a = 3
print s
func()
通過執(zhí)行python pyc_generator.py test
可以生成編譯好的pyc文件。
##pyc_generator.py
import imp
import sys
def generate_pyc(name):
fp, pathname, description = imp.find_module(name)
try:
imp.load_module(name, fp, pathname, description)
finally:
if fp:
fp.close()
if __name__ == "__main__":
generate_pyc(sys.argv[1])
得到test.pyc后疤剑,執(zhí)行hexdump -C test.pyc
可以得到如下二進(jìn)制字符流滑绒。
00000000 03 f3 0d 0a f6 e9 38 55 63 00 00 00 00 00 00 00 |......8Uc.......|
00000010 00 01 00 00 00 40 00 00 00 73 1a 00 00 00 64 00 |.....@...s....d.|
00000020 00 5a 00 00 64 01 00 84 00 00 5a 01 00 65 01 00 |.Z..d.....Z..e..|
00000030 83 00 00 01 64 02 00 53 28 03 00 00 00 74 05 00 |....d..S(....t..|
00000040 00 00 68 65 6c 6c 6f 63 00 00 00 00 01 00 00 00 |..helloc........|
00000050 01 00 00 00 43 00 00 00 73 0f 00 00 00 64 01 00 |....C...s....d..|
00000060 7d 00 00 74 00 00 47 48 64 00 00 53 28 02 00 00 |}..t..GHd..S(...|
00000070 00 4e 69 03 00 00 00 28 01 00 00 00 74 01 00 00 |.Ni....(....t...|
00000080 00 73 28 01 00 00 00 74 01 00 00 00 61 28 00 00 |.s(....t....a(..|
00000090 00 00 28 00 00 00 00 73 1e 00 00 00 2f 55 73 65 |..(....s..../Use|
000000a0 72 73 2f 73 73 6a 2f 50 72 6f 67 2f 70 79 74 68 |rs/ssj/Prog/pyth|
000000b0 6f 6e 2f 74 65 73 74 2e 70 79 74 04 00 00 00 66 |on/test.pyt....f|
000000c0 75 6e 63 03 00 00 00 73 04 00 00 00 00 01 06 01 |unc....s........|
000000d0 4e 28 02 00 00 00 52 01 00 00 00 52 03 00 00 00 |N(....R....R....|
000000e0 28 00 00 00 00 28 00 00 00 00 28 00 00 00 00 73 |(....(....(....s|
000000f0 1e 00 00 00 2f 55 73 65 72 73 2f 73 73 6a 2f 50 |..../Users/ssj/P|
00000100 72 6f 67 2f 70 79 74 68 6f 6e 2f 74 65 73 74 2e |rog/python/test.|
00000110 70 79 74 08 00 00 00 3c 6d 6f 64 75 6c 65 3e 01 |pyt....<module>.|
00000120 00 00 00 73 04 00 00 00 06 02 09 04 |...s........|
0000012c
2.PyCodeObject結(jié)構(gòu)
PyCodeObject格式如下:
這個(gè)圖片轉(zhuǎn)自UC技術(shù)博客,參見參考資料1隘膘。當(dāng)然這個(gè)圖片還有些字段沒有寫出來疑故,比如co_names, co_varnames, co_freevars, co_cellvars,co_filename, co_name, co_firstlineno, co_lnotab。
3.Pyc格式解析
首先4個(gè)字節(jié)是magic number弯菊,03f30d0a 其中0d0a就是\r\n了接下來4個(gè)字節(jié)是時(shí)間纵势,這里是d2e73855,注意到是小端模式,所以實(shí)際是0x5538e7d2,可以發(fā)現(xiàn)是我開始編譯的時(shí)間钦铁。然后就是PyCodeObject對(duì)象了软舌。首先是對(duì)象標(biāo)識(shí)TYPE_CODE,也就是字符c牛曹,值為99佛点,即0x63.然后4個(gè)字節(jié)是全局code block的位置參數(shù)個(gè)數(shù)co_argument,這里是0.再接著4個(gè)字節(jié)是全局code block中的局部變量個(gè)數(shù)co_nlocals黎比,這里是0.接著4個(gè)字節(jié)是code block需要的棾空間co_stacksize,這里值為1.然后4個(gè)字節(jié)是co_flags阅虫,這里是64.
接下來從0x73開始就是code block的字節(jié)碼序列co_code演闭。注意到它是PyStringObject形式存在,因此颓帝,開始寫PyStringObject米碰,注意到首先寫1個(gè)字節(jié)的類型標(biāo)識(shí)TYPE_STRING, 即s购城,對(duì)應(yīng)0x73见间。然后4個(gè)字節(jié)標(biāo)識(shí)長(zhǎng)度為1a,也就是26個(gè)字節(jié)工猜。從0x64開始就是co_code內(nèi)容了杆兵。通過dis命令來看一下內(nèi)容:
In [39]: source = open("test.py").read()
In [40]: co = compile(source, 'test.py', 'exec')
In [41]: co.co_consts
Out[41]: ('hello', <code object func at 0x1075710a8, file "test.py", line 3>, None)
In [42]: co.co_names
Out[42]: ('s', 'func')
In [38]: dis.dis(co)
1 0 LOAD_CONST 0 ('hello') #將co.co_consts[0]即'hello'壓棧
3 STORE_NAME 0 (s) #以co.co_names[0]作為key屋彪,將'hello'出棧,然后設(shè)置f->f_locas['s'] = 'hello'
3 6 LOAD_CONST 1 (<code object func at 0x1075710a8, file "test.py", line 3>) ##將co.co_consts[1]即func的字節(jié)碼對(duì)象壓棧
9 MAKE_FUNCTION 0 ##創(chuàng)建函數(shù)對(duì)象并壓棧
12 STORE_NAME 1 (func) #f->f_locals['func']=函數(shù)對(duì)象
7 15 LOAD_NAME 1 (func) #將f->f_locals['func']即函數(shù)對(duì)象壓棧
18 CALL_FUNCTION 0 #調(diào)用函數(shù),在新棧幀執(zhí)行
21 POP_TOP ##函數(shù)返回值出棧
22 LOAD_CONST 2 (None) ##None壓棧
25 RETURN_VALUE ##返回None
果然是正好26個(gè)字節(jié)跌造,其中內(nèi)容分別對(duì)應(yīng)這些指令担钮,其中第一列是在源碼中的行數(shù)刀脏,第二列是該指令在co_code中的偏移止吐,第三列是opcode,分為有操作數(shù)和無操作數(shù)兩種箭昵,是一個(gè)字節(jié)的整數(shù)税朴。第四列是操作數(shù),占兩個(gè)字節(jié)家制。
那么這些指令對(duì)應(yīng)的就是我們看到的pyc文件中的內(nèi)容了正林,具體意義參見代碼中的注釋。LOAD_CONST指令為0x64颤殴,然后兩個(gè)字節(jié)操作數(shù)是0.接下來是STORE_NAME指令0x5a觅廓,操作數(shù)是0.其他以此類推,frame相關(guān)內(nèi)容后面再解析涵但。
接下來從0x28開始是co.co_consts內(nèi)容杈绸,我們知道這是一個(gè)PyTupleObject對(duì)象帖蔓,保存著code block的常量,如在前面看到的那樣瞳脓,我們知道它有3個(gè)元素塑娇,分別是字符串hello,code object對(duì)象func以及None劫侧。那么PyTupleObject跟PyListObject類似埋酬,首先是記錄類型標(biāo)示TYPE_TUPLE,即'(',也就是0x28了板辽。接下來4個(gè)字節(jié)是長(zhǎng)度奇瘦,這里是3表示有3個(gè)元素棘催。然后是元素內(nèi)容劲弦,第一個(gè)是'hello‘,它是PyStringObject對(duì)象,因此醇坝,先寫入標(biāo)記TYPE_INTERNED,即't'邑跪,也就是上面的0x74了,然后呢呼猪,是寫入4個(gè)字節(jié)的長(zhǎng)度画畅,共5個(gè)字節(jié),所以這是5宋距,接著就是hello這5個(gè)字節(jié)轴踱。
第二個(gè)是code object,好吧谚赎,這個(gè)就相當(dāng)于跟之前的流程再來一遍了淫僻。
0x63跟之前的一樣是TYPE_CODE的標(biāo)示'c',然后就是code object的各個(gè)字段了壶唤。還是來一遍雳灵,分別如下
First Header | Second Header |
---|---|
co_argcount | 0 |
co_nlocals | 1 |
co_stacksize | 1 |
co_flags | 67 |
co_code | 標(biāo)示0x73,即TYPE_STRING闸盔。長(zhǎng)度0x0f悯辙,即15個(gè)字節(jié)長(zhǎng)度。然后從0x64開始就是co_code內(nèi)容迎吵。 |
下面分析下func的co_code躲撰,首先看下dis的結(jié)果:
In [63]: func.co_nlocals
Out[63]: 1
In [64]: func.co_consts
Out[64]: (None, 3)
In [65]: func.co_names
Out[65]: ('s',)
In [66]: func.co_varnames
Out[66]: ('a',)
In [62]: dis.dis(func)
4 0 LOAD_CONST 1 (3) #將func.co_consts[1]即3壓棧
3 STORE_FAST 0 (a) #存儲(chǔ)3在變量a中
5 6 LOAD_GLOBAL 0 (s) #壓入全局變量s
9 PRINT_ITEM ##打印s
10 PRINT_NEWLINE ##打印換行
11 LOAD_CONST 0 (None) #None壓棧
14 RETURN_VALUE ##函數(shù)返回None
接下來就是func的co_consts字段了,同樣是PyTupleObject對(duì)象击费,先是類型標(biāo)示0x28茴肥,然后4個(gè)字節(jié)為長(zhǎng)度2.接著第一個(gè)元素是None(N),即0x4e荡灾,然后是第二個(gè)元素3瓤狐,類型標(biāo)示是TYPE_INT(i)瞬铸,即0x69.后面4個(gè)字節(jié)是整數(shù)3.
再接著就是co_names,同樣是PyTupleObject對(duì)象础锐,顯示標(biāo)示0x28嗓节,然后4個(gè)字節(jié)為長(zhǎng)度1,然后字符s是TYPE_INTERNED類型皆警,于是接著是標(biāo)示't'拦宣,即0x74,然后是字符內(nèi)容s(0x73)信姓。
接下來是co_varnames鸵隧,同樣是PyTupleObject,類型是0x28意推,然后4個(gè)字節(jié)為長(zhǎng)度1豆瘫,然后是字符a。
再后面是閉包相關(guān)的東西co_freevars菊值,為空的PyTupleObject外驱,類型0x28后面4個(gè)字節(jié)長(zhǎng)度為0.
然后是code block內(nèi)部嵌套函數(shù)引用的局部變量名集合co_cellvars,同樣是空的PyTupleObject對(duì)象腻窒。
接著0x73開始就是co_filename了昵宇,這是PyStringObject對(duì)象,先是對(duì)象標(biāo)示s儿子,然后是長(zhǎng)度30.后面是對(duì)應(yīng)的文件的完整路徑"/Users/ssj/Prog/python/test.py"瓦哎。
接著是co_name,即函數(shù)名或者類名柔逼,這里就是func了蒋譬,首先也是對(duì)象標(biāo)示't'(0x74),后面跟著長(zhǎng)度4卒落,然后是’func‘這四個(gè)字節(jié)羡铲。
然后是co_firstlineno,這里直接寫的整數(shù)3.
然后是字節(jié)碼指令與源文件行號(hào)對(duì)應(yīng)關(guān)系co_lnotab儡毕,以PyStringObject對(duì)象存儲(chǔ)也切。先是標(biāo)示's'(0x73),然后是長(zhǎng)度4個(gè)字節(jié)腰湾,然后是內(nèi)容0x00010601.
好吧雷恃,至此,func這個(gè)code object分析完成费坊。我們回到全局的code object倒槐。
全局code object從co_consts[2]開始,這是None,如前面一樣附井,標(biāo)示為0x4e讨越。接著就是co_names两残,co_varnames等,分析跟前面func的類似把跨,不再贅述人弓。注意的是這里的co_names對(duì)應(yīng)的's'和’func'類型不再是TYPE_INTERNED,而是TYPE_STRINGREF('R'),值是0x52.還有就是co_lnotab是0x06020904着逐。
4.參考資料
- Python程序的執(zhí)行原理(好文崔赌,精簡(jiǎn)到位,抓住了重點(diǎn))
- 陳儒《Python源碼剖析》(內(nèi)容很多耸别,有時(shí)間值得慢慢研究的好書)