python虛擬機器怎麼使用

WBOY
發布: 2023-05-15 19:31:15
轉載
1269 人瀏覽過

python 字節碼設計

一條python 字節碼主要有兩部分組成,一部分是操作碼,一部分是這個操作碼的參數,在cpython 當中只有部分字節碼有參數,如果對應的字節碼沒有參數,那麼oparg 的值就等於0 ,在cpython 當中opcode

python虛擬機器怎麼使用

opcode 和 oparg 各佔一個位元組,cpython 虛擬機器使用小端方式保存字節碼。

我們使用下面的程式碼片段先了解一下字節碼的設計:

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 是函數add 的字節碼,是一個位元組序列,list(bytearray(add.__code__.co_code)) 是將和這個序列一個位元組一個位元組進行分開,並將其變成10 進位形式。根據前面我們談到的每一則指令——字節碼佔用2 個字節,因此上面的字節碼有四個指令:
python虛擬機器怎麼使用

操作碼和對應的操作指令在文末有詳細的對應表。在上面的程式碼當中主要使用到了三個字節碼指令分別是 124,23 和 83 ,他們對應的操作指令分別為 LOAD_FAST,BINARY_ADD,RETURN_VALUE。他們的意義如下:

LOAD_FAST:將 varnames[var_num] 壓入堆疊頂端。 BINARY_ADD:從堆疊中彈出兩個物件並且將它們相加的結果壓入堆疊頂部。 RETURN_VALUE:彈出堆疊頂部的元素,將其作為函數的傳回值。

首先我們需要知道的是 BINARY_ADD 和 RETURN_VALUE,這兩個操作指令是沒有參數的,因此在這兩個操作碼之後的參數都是 0 。

但是 LOAD_FAST 是有參數的,在上面我們已經知道 LOAD_FAST 是將 co-varnames[var_num] 壓入堆疊,var_num 就是指令 LOAD_FAST 的參數。在上面的程式碼當中一共有兩個 LOAD_FAST 指令,分別是將 a 和 b 壓入到堆疊中,他們在 varnames 當中的下標分別是 0 和 1,因此他們的操作數就是 0 和 1 。

字節碼擴充參數

在上面我們談到的python 字節碼運算元和操作碼各佔一個字節,但是如果varnames 或常數量表的資料的個數大於1 個位元組的表示範圍的話那麼改如何處理呢?

為了解決這個問題,cpython 為字節碼設計的擴展參數,比如說我們要載入常量表當中的下標為66113 的對象,那麼對應的字節碼如下:

[144, 1, 144, 2, 100, 65]
登入後複製

其中144 表示EXTENDED_ARG,他本質上不是一個python 虛擬機器需要執行的字節碼,這個欄位設計出來主要是為了用與計算擴充參數的。

100 對應的操作指令是LOAD_CONST ,其操作碼是65,但是上面的指令並不會載入常量表當中下標為65 對象,而是會載入下標為66113 的對象,原因就是因為EXTENDED_ARG 。

現在來模擬一下上面的分析過程:

先讀取一條字節碼指令,操作碼等於144 ,說明是擴充參數,那麼此時的參數arg 就等於(1 x (1 << 8)) = 256 。讀第二條字節碼指令,操作碼等於144 ,說明是擴充參數,因為前面arg 已經存在切不等於0 了,那麼此時arg 的計算方式已經改變,arg = arg << 8 2 << 8 ,也就是說原來的arg 乘以256 再加上新的運算元乘以256 ,此時arg = 66048 。讀取第三條字節碼指令,操作碼等於 100,此時是 LOAD_CONST 這條指令,那麼此時的操作碼等於 arg = 65,因為操作碼不是 EXTENDED_ARG 因此操作數不需要在乘以 256 了。

上面的計算過程以程式碼表示如下,下面的程式碼當中 code 就是真正的位元組序列 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)
登入後複製

我們可以使用程式碼來驗證我們前面的分析:

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)
登入後複製

上面的程式碼輸出結果如下所示:

b'\x90\x01\x90\x02dA'
          0 EXTENDED_ARG             1
          2 EXTENDED_ARG           258
          4 LOAD_CONST           66113 (66113)
登入後複製

根據上面程式的輸出結果可以看到我們的分析結果是正確的。

原始碼字節碼映射表

在本小節主要分析一個 code object 物件當中的 co_lnotab 字段,透過分析一個特定的字段來學習這個字段的設計。

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 的輸出第一列是字節碼對應的源代碼的行號,第二列是字節碼在字節序列當中的位移。

上面的程式碼輸出結果如下所示:

  源代码的行号  字节码的位移
  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
登入後複製

從上面程式碼的輸出結果可以看出字節碼一共分成三段,每段表示一行程式碼的字節碼。現在我們來分析一下 co_lnotab 這個字段,這個字段其實也是兩個位元組為一段的。例如上面的 [0, 1, 8, 1, 8, 1] 就可以分成三段 [0, 1], [8, 1], [8, 1] 。這其中的意義分別為:

第一个数字表示距离上一行代码的字节码数目。 第二个数字表示距离上一行有效代码的行数。

现在我们来模拟上面代码的字节码的位移和源代码行数之间的关系:

[0, 1],说明这行代码离上一行代码的字节位移是 0 ,因此我们可以看到使用 dis 输出的字节码 LOAD_FAST ,前面的数字是 0,距离上一行代码的行数等于 1 ,代码的第一行的行号等于 5,因此 LOAD_FAST 对应的行号等于 5 + 1 = 6 。 [8, 1],说明这行代码距离上一行代码的字节位移为 8 个字节,因此第二块的 LOAD_FAST 前面是 8 ,距离上一行代码的行数等于 1,因此这个字节码对应的源代码的行号等于 6 + 1 = 7。 [8, 1],同理可以知道这块字节码对应源代码的行号是 8 。

现在有一个问题是当两行代码之间相距的行数超过 一个字节的表示范围怎么办?在 python3.5 以后如果行数差距大于 127,那么就使用 (0, 行数) 对下一个组合进行表示,(0, \(x_1\)), (0,$ x_2$) ... ,直到 \(x_1 + ... + x_n\) = 行数。

在后面的程序当中我们会使用 compile 这个 python 内嵌函数。当你使用Python编写代码时,可以使用compile()函数将Python代码编译成字节代码对象。这个字节码对象可以被传递给Python的解释器或虚拟机,以执行代码。

compile()函数接受三个参数:

source: 要编译的Python代码,可以是字符串,字节码或AST对象。 filename: 代码来源的文件名(如果有),通常为字符串。 mode: 编译代码的模式。可以是 'exec'、'eval' 或 'single' 中的一个。'exec' 模式用于编译多行代码,'eval' 用于编译单个表达式,'single' 用于编译单行代码。

import dis

code = """
x=1
y=2
""" \
+ "\n" * 500 + \
"""
z=x+y
"""

code = compile(code, '', 'exec')
print(list(bytearray(code.co_lnotab)))
print(code.co_firstlineno)
dis.dis(code)
登入後複製

上面的代码输出结果如下所示:

[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
登入後複製

根据我们前面的分析因为第三行和第二行之间的差距大于 127 ,因此后面的多个组合都是用于表示行数的。

505 = 3(前面已经有三行了) + (127 + 127 + 127 + 121)(这个是第二行和第三行之间的差距,这个值为 502,中间有 500 个换行但是因为字符串相加的原因还增加了两个换行,因此一共是 502 个换行)。

具体的算法用代码表示如下所示,下面的参数就是我们传递给 dis 模块的 code,也就是一个 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_TOP1
ROT_TWO2
ROT_THREE3
DUP_TOP4
DUP_TOP_TWO5
ROT_FOUR6
NOP9
UNARY_POSITIVE10
UNARY_NEGATIVE11
UNARY_NOT12
UNARY_INVERT15
BINARY_MATRIX_MULTIPLY16
INPLACE_MATRIX_MULTIPLY17
BINARY_POWER19
BINARY_MULTIPLY20
BINARY_MODULO22
BINARY_ADD23
BINARY_SUBTRACT24
BINARY_SUBSCR25
BINARY_FLOOR_DIVIDE26
BINARY_TRUE_DIVIDE27
INPLACE_FLOOR_DIVIDE28
INPLACE_TRUE_DIVIDE29
RERAISE48
WITH_EXCEPT_START49
GET_AITER50
GET_ANEXT51
BEFORE_ASYNC_WITH52
END_ASYNC_FOR54
INPLACE_ADD55
INPLACE_SUBTRACT56
INPLACE_MULTIPLY57
INPLACE_MODULO59
STORE_SUBSCR60
DELETE_SUBSCR61
BINARY_LSHIFT62
BINARY_RSHIFT63
BINARY_AND64
BINARY_XOR65
BINARY_OR66
INPLACE_POWER67
GET_ITER68
GET_YIELD_FROM_ITER69
PRINT_EXPR70
LOAD_BUILD_CLASS71
YIELD_FROM72
GET_AWAITABLE73
LOAD_ASSERTION_ERROR74
INPLACE_LSHIFT75
INPLACE_RSHIFT76
INPLACE_AND77
INPLACE_XOR78
INPLACE_OR79
LIST_TO_TUPLE82
RETURN_VALUE83
IMPORT_STAR84
SETUP_ANNOTATIONS85
YIELD_VALUE86
POP_BLOCK87
POP_EXCEPT89
STORE_NAME90
DELETE_NAME91
UNPACK_SEQUENCE92
FOR_ITER93
UNPACK_EX94
STORE_ATTR95
DELETE_ATTR96
STORE_GLOBAL97
DELETE_GLOBAL98
LOAD_CONST100
LOAD_NAME101
BUILD_TUPLE102
BUILD_LIST103
BUILD_SET104
BUILD_MAP105
LOAD_ATTR106
COMPARE_OP107
IMPORT_NAME108
IMPORT_FROM109
JUMP_FORWARD110
JUMP_IF_FALSE_OR_POP111
JUMP_IF_TRUE_OR_POP112
JUMP_ABSOLUTE113
POP_JUMP_IF_FALSE114
POP_JUMP_IF_TRUE115
LOAD_GLOBAL116
IS_OP117
CONTAINS_OP118
JUMP_IF_NOT_EXC_MATCH121
SETUP_FINALLY122
LOAD_FAST124
STORE_FAST125
DELETE_FAST126
RAISE_VARARGS130
CALL_FUNCTION131
MAKE_FUNCTION132
BUILD_SLICE133
LOAD_CLOSURE135
LOAD_DEREF136
STORE_DEREF137
DELETE_DEREF138
CALL_FUNCTION_KW141
CALL_FUNCTION_EX142
SETUP_WITH143
LIST_APPEND145
SET_ADD146
MAP_ADD147
LOAD_CLASSDEREF148
EXTENDED_ARG144
SETUP_ASYNC_WITH154
FORMAT_VALUE155
BUILD_CONST_KEY_MAP156
BUILD_STRING157
LOAD_METHOD160
CALL_METHOD161
LIST_EXTEND162
SET_UPDATE163
DICT_MERGE164
DICT_UPDATE165
#

以上是python虛擬機器怎麼使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:yisu.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!