python虛擬機(jī)pyc文件結(jié)構(gòu)的深入理解
深入理解 PYTHON 虛擬機(jī):PYC 文件結(jié)構(gòu)
在本篇文章當(dāng)中主要給大家介紹一下 .py 文件在被編譯之后對應(yīng)的 pyc 文件結(jié)構(gòu),pyc 文件當(dāng)中的一個核心內(nèi)容就是 python 字節(jié)碼。
PYC 文件
pyc 文件是 Python 在解釋執(zhí)行源代碼時生成的一種字節(jié)碼文件,它包含了源代碼的編譯結(jié)果和相關(guān)的元數(shù)據(jù)信息,以便于 Python 可以更快地加載和執(zhí)行代碼。
Python 是一種解釋型語言,它不像編譯型語言那樣將源代碼直接編譯成機(jī)器碼執(zhí)行。Python 的解釋器會在運(yùn)行代碼之前先將源代碼編譯成字節(jié)碼,然后將字節(jié)碼解釋執(zhí)行。.pyc 文件就是這個過程中生成的字節(jié)碼文件。
當(dāng) Python 解釋器首次執(zhí)行一個 .py 文件時,它會在同一目錄下生成一個對應(yīng)的 .pyc 文件,以便于下次加載該文件時可以更快地執(zhí)行。如果源文件在修改之后被重新加載,解釋器會重新生成 .pyc 文件以更新緩存的字節(jié)碼。
生成 PYC 文件
正常的 python 文件需要通過編譯器變成字節(jié)碼,然后將字節(jié)碼交給 python 虛擬機(jī),然后 python 虛擬機(jī)會執(zhí)行字節(jié)碼。整體流程如下所示:
我們可以直接使用 compile all 模塊生成對應(yīng)文件的 pyc 文件。
? pvm ls demo.py hello.py ? pvm python -m compileall . Listing '.'... Listing './.idea'... Listing './.idea/inspectionProfiles'... Compiling './demo.py'... Compiling './hello.py'... ? pvm ls __pycache__ demo.py hello.py ? pvm ls __pycache__ demo.cpython-310.pyc hello.cpython-310.pyc
python -m compileall .
命令將遞歸掃描當(dāng)前目錄下面的 py 文件,并且生成對應(yīng)文件的 pyc 文件。
PYC 文件布局
第一部分魔數(shù)由兩部分組成:
第一部分 魔術(shù)是由一個 2 字節(jié)的整數(shù)和另外兩個字符回車換行組成的, "\r\n" 也占用兩個字節(jié),一共是四個字節(jié)。這個兩個字節(jié)的整數(shù)在不同的 python 版本還不一樣,比如說在 python3.5 當(dāng)中這個值為 3351 等值,在 python3.9 當(dāng)中這個值為 3420,3421,3422,3423,3424等值(在 python 3.9 的小版本)。
第二部分 Bit Field 這個字段的主要作用是為了將來能夠?qū)崿F(xiàn)復(fù)現(xiàn)編譯結(jié)果,但是在 python3.9a2 時,這個字段的值還全部是 0 。詳細(xì)內(nèi)容可以參考 PEP552-Deterministic pycs 。這個字段在 python2 和 python3 早期版本并沒有(python3.5 還沒有),在 python3 的后期版本這個字段才出現(xiàn)的。
第三部分 就是整個 py 源文件的大小了。
第四部分 也是整個 pyc 文件當(dāng)中最重要的一個部分,最后一個部分就是一個 CodeObject 對象序列化之后的數(shù)據(jù),我們稍后再來仔細(xì)分析一下這個對象相關(guān)的數(shù)據(jù)。
我們現(xiàn)在來具體分析一個 pyc 文件,對應(yīng)的 python 代碼為:
def f(): x = 1 return 2
pyc 文件的十六進(jìn)制形式如下所示:
? __pycache__ hexdump -C hello.cpython-310.pyc 00000000 6f 0d 0d 0a 00 00 00 00 b9 48 21 64 20 00 00 00 |o........H!d ...| 00000010 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 00 02 00 00 00 40 00 00 00 73 0c 00 00 00 64 00 |.....@...s....d.| 00000030 64 01 84 00 5a 00 64 02 53 00 29 03 63 00 00 00 |d...Z.d.S.).c...| 00000040 00 00 00 00 00 00 00 00 00 01 00 00 00 01 00 00 |................| 00000050 00 43 00 00 00 73 08 00 00 00 64 01 7d 00 64 02 |.C...s....d.}.d.| 00000060 53 00 29 03 4e e9 01 00 00 00 e9 02 00 00 00 a9 |S.).N...........| 00000070 00 29 01 da 01 78 72 03 00 00 00 72 03 00 00 00 |.)...xr....r....| 00000080 fa 0a 2e 2f 68 65 6c 6c 6f 2e 70 79 da 01 66 01 |.../hello.py..f.| 00000090 00 00 00 73 04 00 00 00 04 01 04 01 72 06 00 00 |...s........r...| 000000a0 00 4e 29 01 72 06 00 00 00 72 03 00 00 00 72 03 |.N).r....r....r.| 000000b0 00 00 00 72 03 00 00 00 72 05 00 00 00 da 08 3c |...r....r......<| 000000c0 6d 6f 64 75 6c 65 3e 01 00 00 00 73 02 00 00 00 |module>....s....| 000000d0 0c 00 |..| 000000d2
因?yàn)閿?shù)據(jù)使用小端表示方式,因此對于上面的數(shù)據(jù)來說:
- 第一部分魔數(shù)為:0xa0d0d6f 。
- 第二部分 Bit Field 為:0x0 。
- 第三部分最后一次修改日期為:0x642148b9 。
- 第四部分文件大小為:0x20 字節(jié),也就是說 hello.py 這個文件的大小是 32 字節(jié)。
下面是一個小的代碼片段用于讀取 pyc 文件的頭部元信息:
import struct import time import binascii fname = "./__pycache__/hello.cpython-310.pyc" f = open(fname, "rb") magic = struct.unpack('<l', f.read(4))[0] bit_filed = f.read(4) print(f"bit field = {binascii.hexlify(bit_filed)}") moddate = f.read(4) filesz = f.read(4) modtime = time.asctime(time.localtime(struct.unpack('<l', moddate)[0])) filesz = struct.unpack('<L', filesz) print("magic %s" % (hex(magic))) print("moddate (%s)" % (modtime)) print("File Size %d" % filesz) f.close()
上面的代碼輸出結(jié)果如下所示:
bit field = b'00000000'
magic 0xa0d0d6f
moddate (Mon Mar 27 15:41:45 2023)
File Size 32
有關(guān) pyc 文件的詳細(xì)操作可以查看 python 標(biāo)準(zhǔn)庫 importlib/_bootstrap_external.py 文件源代碼。
CODEOBJECT
在 CPython 中,CodeObject
是一個對象,它包含了 Python 代碼的字節(jié)碼、常量、變量、位置參數(shù)、關(guān)鍵字參數(shù)等信息,以及一些用于運(yùn)行代碼的元數(shù)據(jù),如文件名、代碼行號等。
在 CPython 中,當(dāng)我們執(zhí)行一個 Python 模塊或函數(shù)時,解釋器會先將其代碼編譯為 CodeObject
,然后再執(zhí)行。在編譯過程中,解釋器會將 Python 代碼轉(zhuǎn)換為字節(jié)碼,并將其保存在 CodeObject
對象中。此后,每當(dāng)我們調(diào)用該模塊或函數(shù)時,解釋器都會使用 CodeObject
中的字節(jié)碼來執(zhí)行代碼。
CodeObject
對象是不可變的,一旦創(chuàng)建就不能被修改。這是因?yàn)?Python 代碼的字節(jié)碼是不可變的,而 CodeObject
對象包含了這些字節(jié)碼,所以也是不可變的。
在本篇文章當(dāng)中主要介紹 code object 當(dāng)中主要的內(nèi)容,以及簡單介紹他們的作用,在后續(xù)的文章當(dāng)中會仔細(xì)分析 code object 對應(yīng)的源代碼以及對應(yīng)的字段的詳細(xì)作用。
現(xiàn)在舉一個例子來分析一下 pycdemo.py 的 pyc 文件,pycdemo.py 的源程序如下所示:
if __name__ == '__main__': a = 100 print(a)
下面的代碼是一個用于加載 pycdemo01.cpython-39.pyc 文件(也就是 hello.py 對應(yīng)的 pyc 文件)的代碼,使用 marshal 讀取 pyc 文件里面的 code object 。
import marshal import dis import struct import time import types import binascii def print_metadata(fp): magic = struct.unpack('<l', fp.read(4))[0] print(f"magic number = {hex(magic)}") bit_field = struct.unpack('<l', fp.read(4))[0] print(f"bit filed = {bit_field}") t = struct.unpack('<l', fp.read(4))[0] print(f"time = {time.asctime(time.localtime(t))}") file_size = struct.unpack('<l', fp.read(4))[0] print(f"file size = {file_size}") def show_code(code, indent=''): print ("%scode" % indent) indent += ' ' print ("%sargcount %d" % (indent, code.co_argcount)) print ("%snlocals %d" % (indent, code.co_nlocals)) print ("%sstacksize %d" % (indent, code.co_stacksize)) print ("%sflags %04x" % (indent, code.co_flags)) show_hex("code", code.co_code, indent=indent) dis.disassemble(code) print ("%sconsts" % indent) for const in code.co_consts: if type(const) == types.CodeType: show_code(const, indent+' ') else: print(" %s%r" % (indent, const)) print("%snames %r" % (indent, code.co_names)) print("%svarnames %r" % (indent, code.co_varnames)) print("%sfreevars %r" % (indent, code.co_freevars)) print("%scellvars %r" % (indent, code.co_cellvars)) print("%sfilename %r" % (indent, code.co_filename)) print("%sname %r" % (indent, code.co_name)) print("%sfirstlineno %d" % (indent, code.co_firstlineno)) show_hex("lnotab", code.co_lnotab, indent=indent) def show_hex(label, h, indent): h = binascii.hexlify(h) if len(h) < 60: print("%s%s %s" % (indent, label, h)) else: print("%s%s" % (indent, label)) for i in range(0, len(h), 60): print("%s %s" % (indent, h[i:i+60])) if __name__ == '__main__': filename = "./__pycache__/pycdemo01.cpython-39.pyc" with open(filename, "rb") as fp: print_metadata(fp) code_object = marshal.load(fp) show_code(code_object)
執(zhí)行上面的程序輸出結(jié)果如下所示:
magic number = 0xa0d0d61 bit filed = 0 time = Tue Mar 28 02:40:20 2023 file size = 54 code argcount 0 nlocals 0 stacksize 2 flags 0040 code b'650064006b02721464015a01650265018301010064025300' 3 0 LOAD_NAME 0 (__name__) 2 LOAD_CONST 0 ('__main__') 4 COMPARE_OP 2 (==) 6 POP_JUMP_IF_FALSE 20 4 8 LOAD_CONST 1 (100) 10 STORE_NAME 1 (a) 5 12 LOAD_NAME 2 (print) 14 LOAD_NAME 1 (a) 16 CALL_FUNCTION 1 18 POP_TOP >> 20 LOAD_CONST 2 (None) 22 RETURN_VALUE consts '__main__' 100 None names ('__name__', 'a', 'print') varnames () freevars () cellvars () filename './pycdemo01.py' name '<module>' firstlineno 3 lnotab b'08010401'
下面是 code object 當(dāng)中各個字段的作用:
- 首先需要了解一下代碼塊這個概念,所謂代碼塊就是一個小的 python 代碼,被當(dāng)做一個小的單元整體執(zhí)行。在 python 當(dāng)中常見的代碼塊塊有:函數(shù)體、類的定義、一個模塊。
- argcount,這個表示一個代碼塊的參數(shù)個數(shù),這個參數(shù)只對函數(shù)體代碼塊有用,因?yàn)楹瘮?shù)可能會有參數(shù),比如上面的 pycdemo.py 是一個模塊而不是一個函數(shù),因此這個參數(shù)對應(yīng)的值為 0 。
- co_code,這個對象的具體內(nèi)容就是一個字節(jié)序列,存儲真實(shí)的 python 字節(jié)碼,主要是用于 python 虛擬機(jī)執(zhí)行的,在本篇文章當(dāng)中暫時不詳細(xì)分析。
- co_consts,這個字段是一個列表類型的字段,主要是包含一些字符串常量和數(shù)值常量,比如上面的 ";main" 和 100 。
- co_filename,這個字段的含義就是對應(yīng)的源文件的文件名。
- co_firstlineno,這個字段的含義為在 python 源文件當(dāng)中第一行代碼出現(xiàn)的行數(shù),這個字段在進(jìn)行調(diào)試的時候非常重要。
- co_flags,這個字段的主要含義就是標(biāo)識這個 code object 的類型。0x0080 表示這個 block 是一個協(xié)程,0x0010 表示這個 code object 是嵌套的等等。
- co_lnotab,這個字段的含義主要是用于計算每個字節(jié)碼指令對應(yīng)的源代碼行數(shù)。
- co_varnames,這個字段的主要含義是表示在一個 code object 本地定義的一個名字。
- co_names,和 co_varnames 相反,表示非本地定義但是在 code object 當(dāng)中使用的名字。
- co_nlocals,這個字段表示在一個 code object 當(dāng)中本地使用的變量個數(shù)。
- co_stackszie,因?yàn)?python 虛擬機(jī)是一個棧式計算機(jī),這個參數(shù)的值表示這個棧需要的最大的值。
- co_cellvars,co_freevars,這兩個字段主要和嵌套函數(shù)和函數(shù)閉包有關(guān),我們在后續(xù)的文章當(dāng)中將詳細(xì)解釋這個字段。
總結(jié)
在本篇文章當(dāng)中主要給大家介紹了 python 文件被編譯之后的結(jié)果文件 .pyc 文件結(jié)構(gòu),在 pyc 文件當(dāng)中一個最重要的結(jié)構(gòu)就是 code object 對象,在本篇文章當(dāng)中主要是簡單介紹了 code object 各個字段的作用。在后續(xù)的文章當(dāng)中將會舉詳細(xì)的例子進(jìn)行說明,正確理解這些這些字段的含義,對于我們理解 python 虛擬機(jī)大有裨益。
本篇文章是深入理解 python 虛擬機(jī)系列文章之一,文章地址:github.com/Chang-LeHun…
更多精彩內(nèi)容合集可訪問項(xiàng)目:github.com/Chang-LeHun…
以上就是 python 虛擬機(jī):pyc 文件結(jié)構(gòu)的詳細(xì)內(nèi)容,更多關(guān)于 python 虛擬機(jī)pyc文件結(jié)構(gòu)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
python入門:這篇文章帶你直接學(xué)會python
本教程并未涵蓋Python語言的全部內(nèi)容,只是一個入門的教程,Python有非常多的庫以及很多的功能特點(diǎn)需要學(xué)習(xí),小編只是拋磚引玉,希望大家可以從中受益2018-09-09python使用html2text庫實(shí)現(xiàn)從HTML轉(zhuǎn)markdown的方法詳解
這篇文章主要介紹了python使用html2text庫實(shí)現(xiàn)從HTML轉(zhuǎn)markdown的方法,需要的朋友可以參考下2020-02-02基于Python實(shí)現(xiàn)對Excel工作表中的數(shù)據(jù)進(jìn)行排序
在Excel中,排序是整理數(shù)據(jù)的一種重要方式,它可以讓你更好地理解數(shù)據(jù),本文將介紹如何使用第三方庫Spire.XLS?for?Python通過Python來對Excel中的數(shù)據(jù)進(jìn)行排序,需要的可以參考下2024-03-03python selenium 無界面瀏覽器的實(shí)現(xiàn)
有時我們不想讓瀏覽器窗口跳出來,而是想在后臺進(jìn)行操作,這就需要用到無界面瀏覽器,本文主要介紹了python selenium 無界面瀏覽器的實(shí)現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-10-10