您好,登錄后才能下訂單哦!
這篇文章主要介紹“PYTHON虛擬機(jī)的字節(jié)碼設(shè)計(jì)方法是什么”的相關(guān)知識,小編通過實(shí)際案例向大家展示操作過程,操作方法簡單快捷,實(shí)用性強(qiáng),希望這篇“PYTHON虛擬機(jī)的字節(jié)碼設(shè)計(jì)方法是什么”文章能幫助大家解決問題。
在本篇文章當(dāng)中主要給大家介紹 cpython 虛擬機(jī)對于字節(jié)碼的設(shè)計(jì)以及在調(diào)試過程當(dāng)中一個(gè)比較重要的字段 co_lnotab 的設(shè)計(jì)原理!
一條 python 字節(jié)碼主要有兩部分組成,一部分是操作碼,一部分是這個(gè)操作碼的參數(shù),在 cpython 當(dāng)中只有部分字節(jié)碼有參數(shù),如果對應(yīng)的字節(jié)碼沒有參數(shù),那么 oparg 的值就等于 0 ,在 cpython 當(dāng)中 opcode < 90 的指令是沒有參數(shù)的。
opcode 和 oparg 各占一個(gè)字節(jié),cpython 虛擬機(jī)使用小端方式保存字節(jié)碼。
我們使用下面的代碼片段先了解一下字節(jié)碼的設(shè)計(jì):
import dis def add(a, b): return a + b if __name__ == '__main__': print(add.__code__.co_code) print("bytecode: ", list(bytearray(add.__code__.co_code))) dis.dis(add)
上面的代碼在 python3.9 的輸出如下所示:
b'|\x00|\x01\x17\x00S\x00'
bytecode: [124, 0, 124, 1, 23, 0, 83, 0]
5 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
首先 需要了解的是 add.code.co_code 是函數(shù) add 的字節(jié)碼,是一個(gè)字節(jié)序列,list(bytearray(add.__code__.co_code))
是將和這個(gè)序列一個(gè)字節(jié)一個(gè)字節(jié)進(jìn)行分開,并且將其變成 10 進(jìn)制形式。根據(jù)前面我們談到的每一條指令——字節(jié)碼占用 2 個(gè)字節(jié),因此上面的字節(jié)碼有四條指令:
操作碼和對應(yīng)的操作指令在文末有詳細(xì)的對應(yīng)表。在上面的代碼當(dāng)中主要使用到了三個(gè)字節(jié)碼指令分別是 124,23 和 83 ,他們對應(yīng)的操作指令分別為 LOAD_FAST,BINARY_ADD,RETURN_VALUE。他們的含義如下:
LOAD_FAST:將 varnames[var_num] 壓入棧頂。
BINARY_ADD:從棧中彈出兩個(gè)對象并且將它們相加的結(jié)果壓入棧頂。
RETURN_VALUE:彈出棧頂?shù)脑?,將其作為函?shù)的返回值。
首先我們需要知道的是 BINARY_ADD 和 RETURN_VALUE,這兩個(gè)操作指令是沒有參數(shù)的,因此在這兩個(gè)操作碼之后的參數(shù)都是 0 。
但是 LOAD_FAST 是有參數(shù)的,在上面我們已經(jīng)知道 LOAD_FAST 是將 co-varnames[var_num] 壓入棧,var_num 就是指令 LOAD_FAST 的參數(shù)。在上面的代碼當(dāng)中一共有兩條 LOAD_FAST 指令,分別是將 a 和 b 壓入到棧中,他們在 varnames 當(dāng)中的下標(biāo)分別是 0 和 1,因此他們的操作數(shù)就是 0 和 1 。
在上面我們談到的 python 字節(jié)碼操作數(shù)和操作碼各占一個(gè)字節(jié),但是如果 varnames 或者常量表的數(shù)據(jù)的個(gè)數(shù)大于 1 個(gè)字節(jié)的表示范圍的話那么改如何處理呢?
為了解決這個(gè)問題,cpython 為字節(jié)碼設(shè)計(jì)的擴(kuò)展參數(shù),比如說我們要加載常量表當(dāng)中的下標(biāo)為 66113 的對象,那么對應(yīng)的字節(jié)碼如下:
[144, 1, 144, 2, 100, 65]
其中 144 表示 EXTENDED_ARG,他本質(zhì)上不是一個(gè) python 虛擬機(jī)需要執(zhí)行的字節(jié)碼,這個(gè)字段設(shè)計(jì)出來主要是為了用與計(jì)算擴(kuò)展參數(shù)的。
100 對應(yīng)的操作指令是 LOAD_CONST ,其操作碼是 65,但是上面的指令并不會加載常量表當(dāng)中下標(biāo)為 65 對象,而是會加載下標(biāo)為 66113 的對象,原因就是因?yàn)?EXTENDED_ARG 。
現(xiàn)在來模擬一下上面的分析過程:
先讀取一條字節(jié)碼指令,操作碼等于 144 ,說明是擴(kuò)展參數(shù),那么此時(shí)的參數(shù) arg 就等于 (1 x (1 << 8)) = 256 。
讀取第二條字節(jié)碼指令,操作碼等于 144 ,說明是擴(kuò)展參數(shù),因?yàn)榍懊?arg 已經(jīng)存在切不等于 0 了,那么此時(shí) arg 的計(jì)算方式已經(jīng)發(fā)生了改變,arg = arg << 8 + 2 << 8 ,也就是說原來的 arg 乘以 256 再加上新的操作數(shù)乘以 256 ,此時(shí) arg = 66048 。
讀取第三條字節(jié)碼指令,操作碼等于 100,此時(shí)是 LOAD_CONST 這條指令,那么此時(shí)的操作碼等于 arg += 65,因?yàn)椴僮鞔a不是 EXTENDED_ARG 因此操作數(shù)不需要在乘以 256 了。
上面的計(jì)算過程用程序代碼表示如下,下面的代碼當(dāng)中 code 就是真正的字節(jié)序列 HAVE_ARGUMENT = 90 。
def _unpack_opargs(code): extended_arg = 0 for i in range(0, len(code), 2): op = code[i] if op >= HAVE_ARGUMENT: arg = code[i+1] | extended_arg extended_arg = (arg << 8) if op == EXTENDED_ARG else 0 else: arg = None yield (i, op, arg)
我們可以使用代碼來驗(yàn)證我們前面的分析:
import dis def num_to_byte(n): return n.to_bytes(1, "little") def nums_to_bytes(data): ans = b"".join([num_to_byte(n) for n in data]) return ans if __name__ == '__main__': # extended_arg extended_num opcode oparg for python_version > 3.5 bytecode = nums_to_bytes([144, 1, 144, 2, 100, 65]) print(bytecode) dis.dis(bytecode)
上面的代碼輸出結(jié)果如下所示:
b'\x90\x01\x90\x02dA'
0 EXTENDED_ARG 1
2 EXTENDED_ARG 258
4 LOAD_CONST 66113 (66113)
根據(jù)上面程序的輸出結(jié)果可以看到我們的分析結(jié)果是正確的。
在本小節(jié)主要分析一個(gè) code object 對象當(dāng)中的 co_lnotab 字段,通過分析一個(gè)具體的字段來學(xué)習(xí)這個(gè)字段的設(shè)計(jì)。
import dis def add(a, b): a += 1 b += 2 return a + b if __name__ == '__main__': dis.dis(add.__code__) print(f"{list(bytearray(add.__code__.co_lnotab)) = }") print(f"{add.__code__.co_firstlineno = }")
首先 dis 的輸出第一列是字節(jié)碼對應(yīng)的源代碼的行號,第二列是字節(jié)碼在字節(jié)序列當(dāng)中的位移。
上面的代碼輸出結(jié)果如下所示:
源代碼的行號 字節(jié)碼的位移
6 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_FAST 0 (a)
7 8 LOAD_FAST 1 (b)
10 LOAD_CONST 2 (2)
12 INPLACE_ADD
14 STORE_FAST 1 (b)
8 16 LOAD_FAST 0 (a)
18 LOAD_FAST 1 (b)
20 BINARY_ADD
22 RETURN_VALUE
list(bytearray(add.__code__.co_lnotab)) = [0, 1, 8, 1, 8, 1]
add.__code__.co_firstlineno = 5
從上面代碼的輸出結(jié)果可以看出字節(jié)碼一共分成三段,每段表示一行代碼的字節(jié)碼?,F(xiàn)在我們來分析一下 co_lnotab 這個(gè)字段,這個(gè)字段其實(shí)也是兩個(gè)字節(jié)為一段的。比如上面的 [0, 1, 8, 1, 8, 1] 就可以分成三段 [0, 1], [8, 1], [8, 1] 。這其中的含義分別為:
第一個(gè)數(shù)字表示距離上一行代碼的字節(jié)碼數(shù)目。
第二個(gè)數(shù)字表示距離上一行有效代碼的行數(shù)。
現(xiàn)在我們來模擬上面代碼的字節(jié)碼的位移和源代碼行數(shù)之間的關(guān)系:
[0, 1],說明這行代碼離上一行代碼的字節(jié)位移是 0 ,因此我們可以看到使用 dis 輸出的字節(jié)碼 LOAD_FAST ,前面的數(shù)字是 0,距離上一行代碼的行數(shù)等于 1 ,代碼的第一行的行號等于 5,因此 LOAD_FAST 對應(yīng)的行號等于 5 + 1 = 6 。
[8, 1],說明這行代碼距離上一行代碼的字節(jié)位移為 8 個(gè)字節(jié),因此第二塊的 LOAD_FAST 前面是 8 ,距離上一行代碼的行數(shù)等于 1,因此這個(gè)字節(jié)碼對應(yīng)的源代碼的行號等于 6 + 1 = 7。
[8, 1],同理可以知道這塊字節(jié)碼對應(yīng)源代碼的行號是 8 。
現(xiàn)在有一個(gè)問題是當(dāng)兩行代碼之間相距的行數(shù)超過 一個(gè)字節(jié)的表示范圍怎么辦?在 python3.5 以后如果行數(shù)差距大于 127,那么就使用 (0, 行數(shù)) 對下一個(gè)組合進(jìn)行表示,(0, x_1), (0, x_2) ... ,直到 x_1 + ... + x_n = 行數(shù)。
在后面的程序當(dāng)中我們會使用 compile 這個(gè) python 內(nèi)嵌函數(shù)。當(dāng)你使用Python編寫代碼時(shí),可以使用compile()
函數(shù)將Python代碼編譯成字節(jié)代碼對象。這個(gè)字節(jié)碼對象可以被傳遞給Python的解釋器或虛擬機(jī),以執(zhí)行代碼。
compile()
函數(shù)接受三個(gè)參數(shù):
source
: 要編譯的Python代碼,可以是字符串,字節(jié)碼或AST對象。
filename
: 代碼來源的文件名(如果有),通常為字符串。
mode
: 編譯代碼的模式。可以是 'exec'、'eval' 或 'single' 中的一個(gè)。'exec' 模式用于編譯多行代碼,'eval' 用于編譯單個(gè)表達(dá)式,'single' 用于編譯單行代碼。
import dis code = """ x=1 y=2 """ \ + "\n" * 500 + \ """ z=x+y """ code = compile(code, '<string>', 'exec') print(list(bytearray(code.co_lnotab))) print(code.co_firstlineno) dis.dis(code)
上面的代碼輸出結(jié)果如下所示:
[0, 1, 4, 1, 4, 127, 0, 127, 0, 127, 0, 121]
1
2 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (x)
3 4 LOAD_CONST 1 (2)
6 STORE_NAME 1 (y)
505 8 LOAD_NAME 0 (x)
10 LOAD_NAME 1 (y)
12 BINARY_ADD
14 STORE_NAME 2 (z)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
根據(jù)我們前面的分析因?yàn)榈谌泻偷诙兄g的差距大于 127 ,因此后面的多個(gè)組合都是用于表示行數(shù)的。
505 = 3(前面已經(jīng)有三行了) + (127 + 127 + 127 + 121)(這個(gè)是第二行和第三行之間的差距,這個(gè)值為 502,中間有 500 個(gè)換行但是因?yàn)樽址嗉拥脑蜻€增加了兩個(gè)換行,因此一共是 502 個(gè)換行)。
具體的算法用代碼表示如下所示,下面的參數(shù)就是我們傳遞給 dis 模塊的 code,也就是一個(gè) code object 對象。
def findlinestarts(code): """Find the offsets in a byte code which are start of lines in the source. Generate pairs (offset, lineno) as described in Python/compile.c. """ byte_increments = code.co_lnotab[0::2] line_increments = code.co_lnotab[1::2] bytecode_len = len(code.co_code) lastlineno = None lineno = code.co_firstlineno addr = 0 for byte_incr, line_incr in zip(byte_increments, line_increments): if byte_incr: if lineno != lastlineno: yield (addr, lineno) lastlineno = lineno addr += byte_incr if addr >= bytecode_len: # The rest of the lnotab byte offsets are past the end of # the bytecode, so the lines were optimized away. return if line_incr >= 0x80: # line_increments is an array of 8-bit signed integers line_incr -= 0x100 lineno += line_incr if lineno != lastlineno: yield (addr, lineno)
操作 | 操作碼 |
---|---|
POP_TOP | 1 |
ROT_TWO | 2 |
ROT_THREE | 3 |
DUP_TOP | 4 |
DUP_TOP_TWO | 5 |
ROT_FOUR | 6 |
NOP | 9 |
UNARY_POSITIVE | 10 |
UNARY_NEGATIVE | 11 |
UNARY_NOT | 12 |
UNARY_INVERT | 15 |
BINARY_MATRIX_MULTIPLY | 16 |
INPLACE_MATRIX_MULTIPLY | 17 |
BINARY_POWER | 19 |
BINARY_MULTIPLY | 20 |
BINARY_MODULO | 22 |
BINARY_ADD | 23 |
BINARY_SUBTRACT | 24 |
BINARY_SUBSCR | 25 |
BINARY_FLOOR_DIVIDE | 26 |
BINARY_TRUE_DIVIDE | 27 |
INPLACE_FLOOR_DIVIDE | 28 |
INPLACE_TRUE_DIVIDE | 29 |
RERAISE | 48 |
WITH_EXCEPT_START | 49 |
GET_AITER | 50 |
GET_ANEXT | 51 |
BEFORE_ASYNC_WITH | 52 |
END_ASYNC_FOR | 54 |
INPLACE_ADD | 55 |
INPLACE_SUBTRACT | 56 |
INPLACE_MULTIPLY | 57 |
INPLACE_MODULO | 59 |
STORE_SUBSCR | 60 |
DELETE_SUBSCR | 61 |
BINARY_LSHIFT | 62 |
BINARY_RSHIFT | 63 |
BINARY_AND | 64 |
BINARY_XOR | 65 |
BINARY_OR | 66 |
INPLACE_POWER | 67 |
GET_ITER | 68 |
GET_YIELD_FROM_ITER | 69 |
PRINT_EXPR | 70 |
LOAD_BUILD_CLASS | 71 |
YIELD_FROM | 72 |
GET_AWAITABLE | 73 |
LOAD_ASSERTION_ERROR | 74 |
INPLACE_LSHIFT | 75 |
INPLACE_RSHIFT | 76 |
INPLACE_AND | 77 |
INPLACE_XOR | 78 |
INPLACE_OR | 79 |
LIST_TO_TUPLE | 82 |
RETURN_VALUE | 83 |
IMPORT_STAR | 84 |
SETUP_ANNOTATIONS | 85 |
YIELD_VALUE | 86 |
POP_BLOCK | 87 |
POP_EXCEPT | 89 |
STORE_NAME | 90 |
DELETE_NAME | 91 |
UNPACK_SEQUENCE | 92 |
FOR_ITER | 93 |
UNPACK_EX | 94 |
STORE_ATTR | 95 |
DELETE_ATTR | 96 |
STORE_GLOBAL | 97 |
DELETE_GLOBAL | 98 |
LOAD_CONST | 100 |
LOAD_NAME | 101 |
BUILD_TUPLE | 102 |
BUILD_LIST | 103 |
BUILD_SET | 104 |
BUILD_MAP | 105 |
LOAD_ATTR | 106 |
COMPARE_OP | 107 |
IMPORT_NAME | 108 |
IMPORT_FROM | 109 |
JUMP_FORWARD | 110 |
JUMP_IF_FALSE_OR_POP | 111 |
JUMP_IF_TRUE_OR_POP | 112 |
JUMP_ABSOLUTE | 113 |
POP_JUMP_IF_FALSE | 114 |
POP_JUMP_IF_TRUE | 115 |
LOAD_GLOBAL | 116 |
IS_OP | 117 |
CONTAINS_OP | 118 |
JUMP_IF_NOT_EXC_MATCH | 121 |
SETUP_FINALLY | 122 |
LOAD_FAST | 124 |
STORE_FAST | 125 |
DELETE_FAST | 126 |
RAISE_VARARGS | 130 |
CALL_FUNCTION | 131 |
MAKE_FUNCTION | 132 |
BUILD_SLICE | 133 |
LOAD_CLOSURE | 135 |
LOAD_DEREF | 136 |
STORE_DEREF | 137 |
DELETE_DEREF | 138 |
CALL_FUNCTION_KW | 141 |
CALL_FUNCTION_EX | 142 |
SETUP_WITH | 143 |
LIST_APPEND | 145 |
SET_ADD | 146 |
MAP_ADD | 147 |
LOAD_CLASSDEREF | 148 |
EXTENDED_ARG | 144 |
SETUP_ASYNC_WITH | 154 |
FORMAT_VALUE | 155 |
BUILD_CONST_KEY_MAP | 156 |
BUILD_STRING | 157 |
LOAD_METHOD | 160 |
CALL_METHOD | 161 |
LIST_EXTEND | 162 |
SET_UPDATE | 163 |
DICT_MERGE | 164 |
DICT_UPDATE | 165 |
關(guān)于“PYTHON虛擬機(jī)的字節(jié)碼設(shè)計(jì)方法是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會為大家更新不同的知識點(diǎn)。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。