深入理解Python虛擬機(jī)中的反序列化pyc文件
在前面的文章當(dāng)中我們詳細(xì)的對于 pyc 文件的結(jié)構(gòu)進(jìn)行了分析,pyc 文件主要有下面的四個部分組成:魔術(shù)、 Bite Filed 、修改日期和 Code Object 組成。在前面的文章當(dāng)中我們已經(jīng)對前面三個部分進(jìn)行了字節(jié)角度的分析,直接從 pyc 文件當(dāng)中讀取對應(yīng)的數(shù)據(jù)并且打印出來了。而在本篇文章當(dāng)中我們將主要對 Code Object 進(jìn)行分析,并且詳細(xì)它是如何被反序列化的,通過本篇文章我們將能夠把握整個 pyc 文件結(jié)構(gòu)。
marshal 模塊的魔力
序列化和反序列化 python 對象
marshal 是 python 自帶的一個模塊,他可以將一些 python 內(nèi)置對象進(jìn)行序列化和反序列化操作,甚至我們可以在一個文件當(dāng)中序列話一個函數(shù)的 Code Object 對象,然后在另外一個文件反序列化這個 Code Object 對象并且執(zhí)行它。
我們可以使用下面的代碼將 python 當(dāng)中的一些對象序列化操作,直接將 python 對邊變成一個字節(jié)流,保存到磁盤當(dāng)中:
import marshal if __name__ == '__main__': with open("pyobjects.bin", "wb") as fp: marshal.dump(1, fp) marshal.dump(1.5, fp) marshal.dump("Hello World", fp) marshal.dump((1, 2, 3), fp) marshal.dump([1, 2, 3], fp) marshal.dump({1, 2, 3}, fp) marshal.dump(1+1j, fp) marshal.dump({1: 2, 3: 4}, fp)
在上面的代碼當(dāng)中需要注意的是需要使用二進(jìn)制方式 rb 打開文件,上面的程序執(zhí)行完成之后會生成一個 pyobjects.bin 的二進(jìn)制文件,我們可以使用 python 代碼再將上面的 python 對象,比如整數(shù)、浮點數(shù)、字符串和列表元組等等反序列化出來。
import marshal if __name__ == '__main__': with open("pyobjects.bin", "rb") as fp: print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp)) print(marshal.load(fp))
上面的代碼輸出結(jié)果如下所示:
1
1.5
Hello World
(1, 2, 3)
[1, 2, 3]
{1, 2, 3}
(1+1j)
{1: 2, 3: 4}
從上面代碼的輸出結(jié)果我們可以看到我們可以將所有的被寫入到二進(jìn)制文件當(dāng)中的數(shù)據(jù)全部解析了出來。
序列化和反序列化 CodeObject
除了上面使用 marshal 對 python 的基本對象進(jìn)行序列化和反序列化,我們可以使用 marshal 模塊對 CodeObject 進(jìn)行同樣的操作,如果是這樣的話,那么就可以將一個文件的代碼序列化,然后另外一個程序反序列化再進(jìn)行調(diào)用:
import marshal def add(a, b): print("Hello World") return a+b with open("add.bin", "wb") as fp: marshal.dump(add.__code__, fp)
在上面的代碼當(dāng)中,我們打開了文件 add.bin
然后將 add 函數(shù)的 CodeObject 對象寫入到文件當(dāng)中去,而 CodeObject 當(dāng)中保存了函數(shù) add 的所有執(zhí)行所需要的信息,因此我們可以在另外一個文件當(dāng)中打開這個文件,然后將 CodeObject 對象反序列化出來在執(zhí)行這個代碼,我們看下面的代碼:
import marshal def name(): pass with open("add.bin", "rb+") as fp: code = marshal.load(fp) name.__code__ = code print(name(1, 2))
上面的代碼執(zhí)行結(jié)果如下所示:
Hello World
3
可以看到反序列化之后的函數(shù) add 復(fù)制到了 name 上,然后我們調(diào)用了函數(shù) name 真的實現(xiàn)了打印和相加的效果,從這一點來看確實實現(xiàn)了我們在前面所提到的效果。
Python 對象反序列化
在本節(jié)當(dāng)中將主要分析 python 對象序列化之后的二進(jìn)制文件格式,我們到底應(yīng)該如何解析這個文件,解析文件的規(guī)則是什么。在 cpython 當(dāng)中對于每個數(shù)據(jù)類型的解析都是不一樣的,marshal 支持 python 當(dāng)中所有的基本數(shù)據(jù)類型,額外還支持 CodeObject ,在上面的驗證代碼當(dāng)中我們已經(jīng)使用 marshal 去做了一些序列化和反序列化操作。
在對 python 對象進(jìn)行序列化的時候,每一個 python 對象主要是由兩個部分組成的:
其中 type 占一個字節(jié),用于表示接下里啊的 python 的對象類型,比如如字典、元組、集合之類的,而后面的 PyObject 就是實際的 python 數(shù)據(jù)類型了,需要注意的是對于 None、False、True 這種在虛擬機(jī)當(dāng)中只有一個備份的對象,PyObject 是沒有的,也就只有 type 這一個字段。
type 的種類具體如下所示,它只占用一個字節(jié):
class TYPE(Enum): TYPE_NULL = ord('0') TYPE_NONE = ord('N') TYPE_FALSE = ord('F') TYPE_TRUE = ord('T') TYPE_STOPITER = ord('S') TYPE_ELLIPSIS = ord('.') TYPE_INT = ord('i') TYPE_INT64 = ord('I') TYPE_FLOAT = ord('f') TYPE_BINARY_FLOAT = ord('g') TYPE_COMPLEX = ord('x') TYPE_BINARY_COMPLEX = ord('y') TYPE_LONG = ord('l') TYPE_STRING = ord('s') TYPE_INTERNED = ord('t') TYPE_REF = ord('r') TYPE_TUPLE = ord('(') TYPE_LIST = ord('[') TYPE_DICT = ord('{') TYPE_CODE = ord('c') TYPE_UNICODE = ord('u') TYPE_UNKNOWN = ord('?') TYPE_SET = ord('<') TYPE_FROZENSET = ord('>') FLAG_REF = 0x80 TYPE_ASCII = ord('a') TYPE_ASCII_INTERNED = ord('A') TYPE_SMALL_TUPLE = ord(')') TYPE_SHORT_ASCII = ord('z') TYPE_SHORT_ASCII_INTERNED = ord('Z')
我們接下來對上面的類型進(jìn)行一一解釋,首先我們需要了解下面幾個方法,我們在后面的解析過程當(dāng)中會使用到下面的內(nèi)容:
class ByteStreamReader(object): @staticmethod def read_int(buf: bytes): return struct.unpack("<i", buf)[0] @staticmethod def read_byte(buf): return struct.unpack("<B", buf)[0] @staticmethod def read_float(buf): return struct.unpack("<f", buf)[0] @staticmethod def read_double(buf): return struct.unpack("<d", buf)[0] @staticmethod def read_long(buf): return struct.unpack("<q", buf)[0]
上面的的幾個函數(shù)主要是將字節(jié)變成 byte、int 或者浮點數(shù)。接下來我們會實現(xiàn)一個類 PyObjectLoader,用于對 marshal 序列化之后的文件進(jìn)行解析。類的構(gòu)造函數(shù)如下所示:
class PyObjectLoader(object): def __init__(self, filename): self.fp = open(filename, "rb") self.flag = 0 self.refs = []
現(xiàn)在來對一個對象進(jìn)行解析,根據(jù)我們前面談到的內(nèi)容首先我們需要讀入一個字節(jié)的內(nèi)容用于判斷是那種數(shù)據(jù)類型:
在上面的代碼當(dāng)中使用函數(shù) do_parse 對一個 python 對象進(jìn)行解析操作,使用到了 TYPE.FLAG_REF,這個字段的作用表示這個 python 對象是不是一個可引用的,除了 None 、True、False、StopIteration、Ellipsis 是不可引用對象,集合、字典、不可變集合、字符串、字節(jié)、CodeObject 等是可引用對象,可引用對象的 type 的最高位是 1(也就是 type 的第 8 個比特位是 1),非可引用對象就是 0 。如果是可引用對象需要將這個對象加入到引用列表當(dāng)中,因為可能會存在一個對象引用其他對象的情況,需要將對象加入到引用隊列當(dāng)中,如果需要對對象進(jìn)行引用操作直接使用下標(biāo)從引用數(shù)組當(dāng)中查找即可。所有的可引用對象在創(chuàng)建完成之后都需要加入到引用列表當(dāng)中。
- TYPE_NULL,這個在 cpython 虛擬機(jī)當(dāng)中就會直接返回 NULL 。
- TYPE_NONE,返回 python 對象 None 。
- TYPE_FALSE,返回 python 對象 False 。
- TYPE_TRUE,返回 python 對象 True 。
- TYPE_STOPITER,返回 StopIteration 對象。
- TYPE_ELLIPSIS,返回 對象 Ellipsis 。
- TYPE_INT,如果是這個數(shù)據(jù)類型表示接下來的 4 個字節(jié)的數(shù)據(jù)是一個整數(shù)。
- TYPE_INT64,這個類型表示接下來的 8 個字節(jié)表示一個整數(shù)。
- TYPE_BINARY_FLOAT,浮點數(shù)對象,表示接下里啊的 8 個字節(jié)表示一個浮點數(shù)。
- TYPE_BINARY_COMPLEX,復(fù)數(shù)對象,表示接下來有兩個 8 個字節(jié)的浮點數(shù),分別表示實部和虛部。
- TYPE_STRING,這個表示一個 bytes 對象,接下來的四個字節(jié)表示一個整數(shù) size ,整數(shù) size 的含義表示還需要讀取的字節(jié)個數(shù),因此接下來的 size 個字節(jié)就是 bytes 對象的內(nèi)容。
- TYPE_INTERNED,表示一個需要緩存到字符串常量池的字符串,解析方法和 TYPE_STRING 一樣首先讀取四個字節(jié)得到一個整數(shù) size,然后在讀取 size 個字節(jié),表示字符串的內(nèi)容,我們在 python 當(dāng)中可以直接使用
.decode("utf-8")
進(jìn)行編碼。 - TYPE_REF,表示需要引用一個對象,讀取四個字節(jié)作為整數(shù) size,然后從引用列表當(dāng)中獲取下標(biāo)為 size 的對象。
- TYPE_TUPLE,表示一個元組,首先讀取四個字節(jié)的數(shù)據(jù)得到一個整數(shù) size ,然后使用 for 循環(huán)遞歸調(diào)用 do_parse 函數(shù)獲取 size 的對象。
- TYPE_LIST,解析方式和 TYPE_TUPLE 一樣,只不過返回列表對象。
- TYPE_DICT,這個解析的方式不斷的調(diào)用 do_parse 函數(shù),從 1 開始計數(shù),奇數(shù)對象當(dāng)作 key,偶數(shù)對象當(dāng)中 val,直到遇到 NULL,跳出循環(huán)停止解析,這個類型可以直接看下面的解析代碼,非常清晰。
- TYPE_CODE,這個類型表示一個 CodeObject 對象,見下面的解析代碼,這部分代碼可以結(jié)合 CodeObject 的字段分析,前面24 個字節(jié)表示整數(shù)對象,用于表示 CodeObject 的 6 個字段,接下來的是 8 個 PyObject 對象,因此需要調(diào)用 do_parse 函數(shù)進(jìn)行解析,然后再解析一個 4 字節(jié)的整數(shù)表示第一行代碼的行號,最后再讀取一個 PyObject 對象。
- TYPE_UNICODE,表示一個字符串,讀取方式和 TYPE_INTERNED 一樣。
- TYPE_SET,前 4 個自己表示集合當(dāng)中元素的個數(shù) size,接下來使用 for 循環(huán)讀?。ㄕ{(diào)用 do_parse) size 的元素加入到集合當(dāng)中。
- TYPE_FROZENSET,和 TYPE_SET 讀取方式一樣,只不過返回 frozen set 。
- TYPE_ASCII,和 TYPE_UNICODE 讀取方式一樣,也可以使用 utf-8 編碼,雖然讀取的是 ASCII 編碼的字符,但是 utf-8 兼容 ASCII 因此也可以。
- TYPE_ASCII_INTERNED,和 TYPE_ASCII 解析方式一樣。
- TYPE_SMALL_TUPLE,讀取一個字節(jié)的數(shù)據(jù)表示元組當(dāng)中的數(shù)據(jù)個數(shù),然后讀取對應(yīng)個數(shù)的對象。
- TYPE_SHORT_ASCII,之前是讀取四個字節(jié)作為長度,現(xiàn)在只讀取一個字節(jié)作為字節(jié)個數(shù)。
- TYPE_SHORT_ASCII_INTERNED,和 TYPE_SHORT_ASCII 讀取方式一樣,只不過加入到字符串常量池子。
余下的對象的解析不在一一解釋,大家可以直接看下方代碼,都是比較清晰易懂的。
class PyObjectLoader(object): def __init__(self, filename): self.reader = ByteStreamReader() self.fp = open(filename, "rb") self.flag = 0 self.refs = [] def do_parse(self): c = self.fp.read(1) assert len(c) != 0, "can not read more data from file descriptor" t = ByteStreamReader.read_byte(c) & (~TYPE.FLAG_REF.value) self.flag = ByteStreamReader.read_byte(c) & TYPE.FLAG_REF.value match t: case TYPE.TYPE_NULL.value: return None case TYPE.TYPE_NONE.value: return None case TYPE.TYPE_FALSE.value: return False case TYPE.TYPE_TRUE.value: return True case TYPE.TYPE_STOPITER.value: return StopIteration case TYPE.TYPE_ELLIPSIS.value: return Ellipsis case TYPE.TYPE_INT.value: ret = ByteStreamReader.read_int(self.fp.read(4)) self.refs.append(ret) return TYPE.TYPE_INT, ret case TYPE.TYPE_INT64.value: ret = ByteStreamReader.read_long(self.fp.read(8)) self.refs.append(ret) return TYPE.TYPE_INT64, ret case TYPE.TYPE_FLOAT.value: raise RuntimeError("Unsupported TYPE TYPE_FLOAT") case TYPE.TYPE_BINARY_FLOAT.value: ret = ByteStreamReader.read_double(self.fp.read(8)) self.refs.append(ret) return TYPE.TYPE_FLOAT, ret case TYPE.TYPE_COMPLEX.value: raise RuntimeError("Unsupported TYPE TYPE_COMPLEX") case TYPE.TYPE_BINARY_COMPLEX.value: ret = complex(self.do_parse(), self.do_parse()) self.refs.append(ret) return TYPE.TYPE_BINARY_COMPLEX, ret case TYPE.TYPE_LONG.value: raise RuntimeError("Unsupported TYPE TYPE_LONG") case TYPE.TYPE_STRING.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = self.fp.read(size) self.refs.append(ret) return TYPE.TYPE_STRING, ret case TYPE.TYPE_INTERNED.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_INTERNED, ret case TYPE.TYPE_REF.value: size = ByteStreamReader.read_int(self.fp.read(4)) return TYPE.TYPE_REF, self.refs[size] case TYPE.TYPE_TUPLE.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = [] self.refs.append(ret) for i in range(size): ret.append(self.do_parse()) return TYPE.TYPE_TUPLE, tuple(ret) case TYPE.TYPE_LIST.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = [] self.refs.append(ret) for i in range(size): ret.append(self.do_parse()) return TYPE.TYPE_LIST, ret case TYPE.TYPE_DICT.value: ret = dict() self.refs.append(ret) while True: key = self.do_parse() if key is None: break val = self.do_parse() if val is None: break ret[key] = val return TYPE.TYPE_DICT, ret case TYPE.TYPE_CODE.value: ret = dict() idx = len(self.refs) self.refs.append(None) ret["argcount"] = ByteStreamReader.read_int(self.fp.read(4)) ret["posonlyargcount"] = ByteStreamReader.read_int(self.fp.read(4)) ret["kwonlyargcount"] = ByteStreamReader.read_int(self.fp.read(4)) ret["nlocals"] = ByteStreamReader.read_int(self.fp.read(4)) ret["stacksize"] = ByteStreamReader.read_int(self.fp.read(4)) ret["flags"] = ByteStreamReader.read_int(self.fp.read(4)) ret["code"] = self.do_parse() ret["consts"] = self.do_parse() ret["names"] = self.do_parse() ret["varnames"] = self.do_parse() ret["freevars"] = self.do_parse() ret["cellvars"] = self.do_parse() ret["filename"] = self.do_parse() ret["name"] = self.do_parse() ret["firstlineno"] = ByteStreamReader.read_int(self.fp.read(4)) ret["lnotab"] = self.do_parse() self.refs[idx] = ret return TYPE.TYPE_CODE, ret case TYPE.TYPE_UNICODE.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_INTERNED, ret case TYPE.TYPE_UNKNOWN.value: raise RuntimeError("Unknown value " + str(t)) case TYPE.TYPE_SET.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = set() self.refs.append(ret) for i in range(size): ret.add(self.do_parse()) return TYPE.TYPE_SET, ret case TYPE.TYPE_FROZENSET.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = set() idx = len(self.refs) self.refs.append(None) for i in range(size): ret.add(self.do_parse()) self.refs[idx] = ret return TYPE.TYPE_SET, frozenset(ret) case TYPE.TYPE_ASCII.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_INTERNED, ret case TYPE.TYPE_ASCII_INTERNED.value: size = ByteStreamReader.read_int(self.fp.read(4)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_ASCII_INTERNED, ret case TYPE.TYPE_SMALL_TUPLE.value: size = ByteStreamReader.read_byte(self.fp.read(1)) ret = [] self.refs.append(ret) for i in range(size): ret.append(self.do_parse()) return TYPE.TYPE_SMALL_TUPLE, tuple(ret) case TYPE.TYPE_SHORT_ASCII.value: size = ByteStreamReader.read_byte(self.fp.read(1)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_SHORT_ASCII, ret case TYPE.TYPE_SHORT_ASCII_INTERNED.value: size = ByteStreamReader.read_byte(self.fp.read(1)) ret = self.fp.read(size).decode("utf-8") self.refs.append(ret) return TYPE.TYPE_SHORT_ASCII_INTERNED, ret case _: raise RuntimeError("can not parse " + str(t)) def __del_(self): self.fp.close()
我們現(xiàn)在使用下面的代碼生成一些二進(jìn)制文件:
import marshal def add(a, b): print("Hello World") return a+b if __name__ == '__main__': with open("add.bin", "wb") as fp: marshal.dump(add.__code__, fp) with open("int.bin", "wb") as fp: marshal.dump(1, fp) with open("float.bin", "wb") as fp: marshal.dump(1.5, fp) with open("tuple.bin", "wb") as fp: marshal.dump((1, 2, 3), fp) with open("set.bin", "wb") as fp: marshal.dump({1, 2, 3}, fp) with open("list.bin", "wb") as fp: marshal.dump([1, 2, 3], fp) with open("dict.bin", "wb") as fp: marshal.dump({1: 2, 3: 4}, fp) with open("code.bin", "wb") as fp: marshal.dump(add.__code__, fp) with open("string.bin", "wb") as fp: marshal.dump("Hello World", fp)
當(dāng)我們使用 marshal 對函數(shù) add 的 code 進(jìn)行序列化的時候?qū)嶋H上就是序列化一個 CodeObject 對象,這個對象的結(jié)果實際上和 pyc 的結(jié)構(gòu)是一樣的。
我們使用下面的代碼進(jìn)行反序列化:
if __name__ == '__main__': assert sys.version_info.major == 3 and sys.version_info.minor == 10, "only python3.10 works" loader = PyObjectLoader("int.bin") print(loader.do_parse()) loader = PyObjectLoader("float.bin") print(loader.do_parse()) loader = PyObjectLoader("set.bin") print(loader.do_parse()) loader = PyObjectLoader("dict.bin") print(loader.do_parse()) loader = PyObjectLoader("tuple.bin") print(loader.do_parse()) loader = PyObjectLoader("list.bin") print(loader.do_parse()) loader = PyObjectLoader("string.bin") print(loader.do_parse()) loader = PyObjectLoader("code.bin") pprint(loader.do_parse())
需要注意的是本篇文章代碼需要在 python 3.10 上運行,如果需要在 3.8 3.9 運行的話可以將 match 語句改成 if-else 語句。但是由于 python 3.11 當(dāng)中的 CodeObject 對象的字段發(fā)生了一些微小的變化,因此上面的代碼是不能在 python 3.11 上執(zhí)行的。上面的代碼執(zhí)行結(jié)果如下所示:
(<TYPE.TYPE_INT: 105>, 1)
(<TYPE.TYPE_FLOAT: 102>, 1.5)
(<TYPE.TYPE_SET: 60>, {(<TYPE.TYPE_INT: 105>, 1), (<TYPE.TYPE_INT: 105>, 2), (<TYPE.TYPE_INT: 105>, 3)})
(<TYPE.TYPE_DICT: 123>, {(<TYPE.TYPE_INT: 105>, 1): (<TYPE.TYPE_INT: 105>, 2), (<TYPE.TYPE_INT: 105>, 3): (<TYPE.TYPE_INT: 105>, 4)})
(<TYPE.TYPE_SMALL_TUPLE: 41>, ((<TYPE.TYPE_INT: 105>, 1), (<TYPE.TYPE_INT: 105>, 2), (<TYPE.TYPE_INT: 105>, 3)))
(<TYPE.TYPE_LIST: 91>, [(<TYPE.TYPE_INT: 105>, 1), (<TYPE.TYPE_INT: 105>, 2), (<TYPE.TYPE_INT: 105>, 3)])
(<TYPE.TYPE_SHORT_ASCII: 122>, 'Hello World')
(<TYPE.TYPE_CODE: 99>,
{'argcount': 2,
'cellvars': (<TYPE.TYPE_REF: 114>, 'print'),
'code': (<TYPE.TYPE_STRING: 115>,
b't\x00d\x01\x83\x01\x01\x00|\x00|\x01\x17\x00S\x00'),
'consts': (<TYPE.TYPE_SMALL_TUPLE: 41>,
(None, (<TYPE.TYPE_SHORT_ASCII: 122>, 'Hello World'))),
'filename': (<TYPE.TYPE_SHORT_ASCII: 122>,
'/Users/xxxxxxx/Desktop/workdir/dive-into-cpython/code/marshal_demos/add.py'),
'firstlineno': 5,
'flags': 67,
'freevars': (<TYPE.TYPE_SMALL_TUPLE: 41>, ()),
'kwonlyargcount': 0,
'lnotab': (<TYPE.TYPE_STRING: 115>, b'\x08\x01\x08\x01'),
'name': (<TYPE.TYPE_SHORT_ASCII_INTERNED: 90>, 'add'),
'names': (<TYPE.TYPE_SMALL_TUPLE: 41>,
((<TYPE.TYPE_SHORT_ASCII_INTERNED: 90>, 'print'),)),
'nlocals': 2,
'posonlyargcount': 0,
'stacksize': 2,
'varnames': (<TYPE.TYPE_SMALL_TUPLE: 41>,
((<TYPE.TYPE_SHORT_ASCII_INTERNED: 90>, 'a'),
(<TYPE.TYPE_SHORT_ASCII_INTERNED: 90>, 'b')))})
從上面的解析結(jié)果來看我們是實現(xiàn)了正確的解析的。
總結(jié)
在本篇文章當(dāng)中主要給大家分析了 python 對象序列化之后我們該如何反序列化這些對象,并且使用 python 對二進(jìn)制文件進(jìn)行了分析,可以成功的將 python 對象解析出來,但是我們忽略了兩個稍微復(fù)雜一點的對象,他們的解析稍微有點復(fù)雜,但是我們平時的變成當(dāng)中很少使用到,因此本文的代碼解析一般的文件都是可以的。
到此這篇關(guān)于深入理解Python虛擬機(jī)中的反序列化pyc文件的文章就介紹到這了,更多相關(guān)Python虛擬機(jī)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Python內(nèi)置的模塊與函數(shù)進(jìn)行不同進(jìn)制的數(shù)的轉(zhuǎn)換
這篇文章主要介紹了使用Python內(nèi)置的模塊與函數(shù)進(jìn)行不同進(jìn)制的數(shù)的轉(zhuǎn)換的方法,Python也使得讀取純二進(jìn)制文件內(nèi)容非常方便,需要的朋友可以參考下2016-03-03python Django連接MySQL數(shù)據(jù)庫做增刪改查
本文寫的是python Django連接MySQL數(shù)據(jù)庫的步驟,提供增刪改查的代碼2013-11-11OpenCV-Python直方圖均衡化實現(xiàn)圖像去霧
直方圖均衡化可以達(dá)到增強(qiáng)圖像顯示效果的目的。最常用的比如去霧。本文就來實現(xiàn)直方圖均衡化實現(xiàn)圖像去霧,感興趣的可以了解一下2021-06-06torchxrayvision包安裝過程(附pytorch1.6cpu版安裝)
這篇文章主要介紹了torchxrayvision包安裝過程(附pytorch1.6cpu版安裝),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08