国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Python > 正文

深入Python解釋器理解Python中的字節碼

2019-11-25 17:52:39
字體:
來源:轉載
供稿:網友

我最近在參與Python字節碼相關的工作,想與大家分享一些這方面的經驗。更準確的說,我正在參與2.6到2.7版本的CPython解釋器字節碼的工作。

Python是一門動態語言,在命令行工具下運行時,本質上執行了下面的步驟:

  •     當第一次執行到一段代碼時,這段代碼會被編譯(如,作為一個模塊加載,或者直接執行)。根據操作系統的不同,這一步生成后綴名是pyc或者pyo的二進制文件。
  •     解釋器讀取二進制文件,并依次執行指令(opcodes)。

Python解釋器是基于棧的。要理解數據流向,我們需要知道每條指令的棧效應(如,操作碼和參數)。

探索Python二進制文件

得到一個二進制文件字節碼的最簡單方式,是對CodeType結構進行解碼:
 

import marshalfd = open('path/to/my.pyc', 'rb')magic = fd.read(4) # 魔術數,與python版本相關date = fd.read(4) # 編譯日期code_object = marshal.load(fd)fd.close()

code_object包含了一個CodeType對象,它代表被加載文件的整個模塊。為了查看這個模塊的類定義、方法等的所有嵌套編碼對象(編碼對象,原文為code object),我們需要遞歸地檢查CodeType的常量池。就像下面的代碼:
 

import types def inspect_code_object(co_obj, indent=''):print indent, "%s(lineno:%d)" % (co_obj.co_name, co_obj.co_firstlineno)for c in co_obj.co_consts:if isinstance(c, types.CodeType):inspect_code_object(c, indent + ' ') inspect_code_object(code_object) # 從第一個對象開始

這個案例中,我們打印出一顆編碼對象樹,每個編碼對象是其父對象的子節點。對下面的代碼:
 

class A:def __init__(self):passdef __repr__(self):return 'A()'a = A()print a

我們得到的樹形結果是:

<module>(lineno:2) A(lineno:2) __init__(lineno:3) __repr__(lineno:5)

為了測試,我們可以通過compile指令,編譯一個包含Python源碼的字符串,從而能夠得到一個編碼對象:
 

co_obj = compile(python_source_code, '<string>', 'exec')

要獲取更多關于編碼對象的信息,我們可以查閱Python文檔的co_* fields 部分。

初見字節碼

一旦我們得到了編碼對象,我們就可以開始對它進行拆解了(在co_code字段)。從字節碼中解析出它的含義:

  • ? 解釋操作碼的含義
  • ? 提取任意參數

dis模塊的disassemble函數展示了是如何做到的。對我們前面例子,它輸出的結果是:

2 0 LOAD_CONST 0 ('A') 3 LOAD_CONST 3 (()) 6 LOAD_CONST 1 (<code object A at 0x42424242, file "<string>", line 2>) 9 MAKE_FUNCTION 0 12 CALL_FUNCTION 0 15 BUILD_CLASS 16 STORE_NAME 0 (A) 8 19 LOAD_NAME  0 (A) 22 CALL_FUNCTION 0 25 STORE_NAME 1 (a) 9 28 LOAD_NAME  1 (a) 31 PRINT_ITEM 32 PRINT_NEWLINE 33 LOAD_CONST 2 (None) 36 RETURN_VALUE

我們得到了:

  •     行號(當它改變時)
  •     指令的序號
  •     當前指令的操作碼
  •     操作參數(oparg),操作碼用它來計算實際的參數。例如,對于LOAD_NAME操作碼,操作參數指向tuple co_names的索引。
  •     計算后的實際參數(圓括號內)

對于序號為6的指令,操作碼LOAD_CONST的操作參數,指向需要從tuple co_consts加載的對象。這里,它指向A的類型定義。同樣的,我們能夠繼續并反編譯所有的代碼對象,得到模塊的全部字節碼。

字節碼的第一部分(序號0到16),與A的類型定義有關;其他的部分是我們實例化A,并打印它的代碼。

有趣的字節碼構造

所有的操作碼都是相當直接易懂的,但是由于下面的原因,在個別情況下會顯得奇怪:

  •     編譯器優化
  •     解釋器優化(因此會導致加入額外的操作碼)

順序變量賦值

首先,我們看看順序地對多個元素賦值,會發生什么:

(1) a, b = 1, '2'(2) a, b = 1, e(3) a, b, c = 1, 2, e(4) a, b, c, d = 1, 2, 3, e

這4中語句,會產生差別相當大的字節碼。

第一種情況最簡單,因為賦值操作的右值(RHS)只包含常量。這種情況下,CPython會創建一個(1, ‘a') 的t uple,使用UNPACK_SEQUENCE操作碼,把兩個元素壓到棧上,并對變量a和b分別執行STORE_FAST操作:

0 LOAD_CONST 5 ((1, '2'))3 UNPACK_SEQUENCE 26 STORE_FAST 0 (a)9 STORE_FAST 1 (b)

而第二種情況,則在右值引入了一個變量,因此一般情況下,會調用一條取值指令(這里簡單地調用了LOAD_GLOBAL指令)。但是,編譯器不需要在棧上為這些值創建一個新的tuple,也不需要調用UNPACK_SEQUENCE(序號18);調用ROT_TWO就足夠了,它用來交換棧頂的兩個元素(雖然交換指令19和22也可以達到目的)。

12 LOAD_CONST 1 (1)15 LOAD_GLOBAL 0 (e)18 ROT_TWO19 STORE_FAST 0 (a)22 STORE_FAST 1 (b)

第三種情況變得很奇怪。把表達式放到棧上與前一種情況的處理方式相同,但是在交換棧頂的3個元素后,它再次交換了棧頂的2個元素:

25 LOAD_CONST 1 (1)28 LOAD_CONST 3 (2)31 LOAD_GLOBAL 0 (e)34 ROT_THREE35 ROT_TWO36 STORE_FAST 0 (a)39 STORE_FAST 1 (b)42 STORE_FAST 2 (c)

最后一種情況是通用的處理方式,ROT_*操作看起來行不通了,編譯器創建了一個tuple,然后調用UNPACK_SEQUENCE把元素放到棧上:

45 LOAD_CONST 1 (1)48 LOAD_CONST 3 (2)51 LOAD_CONST 4 (3)54 LOAD_GLOBAL 0 (e)57 BUILD_TUPLE 460 UNPACK_SEQUENCE 463 STORE_FAST 0 (a)66 STORE_FAST 1 (b)69 STORE_FAST 2 (c)72 STORE_FAST 3 (d)

函數調用構造

最后一組有趣的例子是關于函數調用構造,以及創建調用的4個操作碼。我猜測這些操作碼的數量是為了優化解釋器代碼,因為它不像Java,有invokedynamic,invokeinterface,invokespecial,invokestatic或者invokevirtual之一。

Java中,invokeinterface,invokespecial和invokevirtual都是從靜態類型語言中借鑒來的(invokespecial只被用來調用構造函數和父類AFAIK)。Invokestatic是自我描述的(不需要把接收方放在棧上),在Python中沒有類似的概念(在解釋器層面上,而不是裝飾者)。簡短的說,Python調用都能被轉換成invokedynamic。

在Python中,不同的CALL_*操作碼確實不存在,原因是類型系統,靜態方法,或者特殊訪問構造器的需求。它們都指向了Python中一個函數調用是如何確定的。從語法來看:

調用結構允許代碼這些寫:
 

func(arg1, arg2, keyword=SOME_VALUE, *unpack_list, **unpack_dict)

關鍵字參數允許通過形式參數的名稱來傳遞參數,而不僅僅是通過位置。*符號從一個可迭代的容器中取出所有元素,作為參數傳入(逐個元素,不是以tuple的形式),而**符號處理一個包含關鍵字和值的字典。

這個例子用到了調用構造的幾乎所有特性:
? 傳遞變量參數列表(_VAR):CALL_FUNCTION_VAR, CALL_FUNCTION_VAR_KW
? 傳遞基于字典的關鍵字(_KW):CALL_FUNCTION_KW, CALL_FUNCTION_VAR_KW

字節碼是這樣的:

0 LOAD_NAME 0 (func)3 LOAD_NAME 1 (arg1)6 LOAD_NAME 2 (arg2)9 LOAD_CONST 0 ('keyword')12 LOAD_NAME 3 (SOME_VALUE)15 LOAD_NAME 4 (unpack_list)18 LOAD_NAME 5 (unpack_dict)21 CALL_FUNCTION_VAR_KW 258

通常,CALL_FUNCTION調用將oparg解析為參數個數。但是,更多的信息被編碼。第一個字節(0xff掩碼)存儲參數的個數,第二個字節((value >> 8) & 0xff)存儲傳遞的關鍵字參數個數。為了要計算需要從棧頂彈出的元素個數,我們需要這么做:
 

na = arg & 0xff # num argsnk = (arg >> 8) & 0xff # num keywordsn_to_pop = na + 2 * nk + CALL_EXTRA_ARG_OFFSET[op]

CALL_EXTRA_ARG_OFFSET包含了一個偏移量,由調用操作碼確定(對CALL_FUNCTION_VAR_KW來說,是2)。這里,在訪問函數名稱前,我們需要彈出6個元素。

對于其他的CALL_*調用,完全依賴于代碼是否使用列表或者字典傳遞參數。只需要簡單的組合即可。

構造一個極小的CFG

為了理解代碼是如何運行的,我們可以構造一個控制流程圖(control-flow graph,CFG),這個過程非常有趣。我們通過它,查看在什么條件下,哪些無條件判斷的操作碼(基本單元)序列會被執行。

即使字節碼是一門真正的小型語言,構造一個運行穩定的CFG需要大量的細節工作,遠超出本博客的范圍。因此如果需要一個真實的CFG實現,你可以看看這里equip

在這里,我們只關注沒有循環和異常的代碼,因此控制流程只依賴與if語句。

只有少數幾個操作碼能夠執行地址跳轉(對沒有循環和異常的情況);它們是:

    JUMP_FORWARD:在字節碼中跳轉到一個相對位置。參數是跳過的字節數。
    JUMP_IF_FALSE_OR_POP,JUMP_IF_TRUE_OR_POP,JUMP_ABSOLUTE,POP_JUMP_IF_FALSE,以及POP_JUMP_IF_TRUE:參數都是字節碼中的絕對地址。

為一個函數夠造CFG,意味著要創建基本的單元(不包含條件判斷的操作碼序列――除非有異常發生),并且把它們與條件和分支連在一起,構成一個圖。在我們的例子中,我們只有True、False和無條件分支。

讓我們來考慮下面的代碼示例(在實際中絕對不要這樣用):
 

def factorial(n):if n <= 1:return 1elif n == 2:return 2return n * factorial(n - 1)

如前所述,我們得到factorial方法的代碼對象:
 

module_co = compile(python_source, '', 'exec')meth_co = module_co.co_consts[0]

反匯編結果是這樣的(<<<后是我的注釋):
 

3  0 LOAD_FAST  0 (n)  3 LOAD_CONST  1 (1)  6 COMPARE_OP  1 (<=)  9 POP_JUMP_IF_FALSE 16  <<< control flow 4  12 LOAD_CONST  1 (1)  15 RETURN_VALUE    <<< control flow 5 >> 16 LOAD_FAST  0 (n)  19 LOAD_CONST  2 (2)  22 COMPARE_OP  2 (==)  25 POP_JUMP_IF_FALSE 32  <<< control flow 6  28 LOAD_CONST  2 (2)  31 RETURN_VALUE    <<< control flow 7 >> 32 LOAD_FAST  0 (n)  35 LOAD_GLOBAL  0 (factorial)  38 LOAD_FAST  0 (n)  41 LOAD_CONST  1 (1)  44 BINARY_SUBTRACT  45 CALL_FUNCTION  1  48 BINARY_MULTIPLY  49 RETURN_VALUE    <<< control flow

在這個字節碼中,我們有5條改變CFG結構的指令(添加約束條件,或者允許快速退出):

    POP_JUMP_IF_FALSE:跳轉到絕對地址16和32;
    RETURN_VALUE:從棧頂彈出一個元素,并返回。

提取基本單元很簡單,因為我們只關心那些改變控制流程的指令。在我們的例子中,我們沒有遇到強制跳轉指令,如JUMP_FORWARD或JUMP_ABSOLUTE。

提取這類結構的代碼示例:

import opcodeRETURN_VALUE = 83JUMP_FORWARD, JUMP_ABSOLUTE = 110, 113FALSE_BRANCH_JUMPS = (111, 114) # JUMP_IF_FALSE_OR_POP, POP_JUMP_IF_FALSE def find_blocks(meth_co): blocks = {} code = meth_co.co_code finger_start_block = 0 i, length = 0, len(code) while i < length: op = ord(code[i]) i += 1 if op == RETURN_VALUE: # We force finishing the block after the return,    # dead code might still exist after though... blocks[finger_start_block] = { 'length': i - finger_start_block - 1, 'exit': True } finger_start_block = i elif op >= opcode.HAVE_ARGUMENT: oparg = ord(code[i]) + (ord(code[i+1]) << 8) i += 2 if op in opcode.hasjabs: # Absolute jump to oparg blocks[finger_start_block] = {  'length': i - finger_start_block } if op == JUMP_ABSOLUTE: # Only uncond absolute jump  blocks[finger_start_block]['conditions'] = {  'uncond': oparg  } else:  false_index, true_index = (oparg, i) if op in FALSE_BRANCH_JUMPS else (i, oparg)  blocks[finger_start_block]['conditions'] = {  'true': true_index,  'false': false_index  } finger_start_block = i elif op in opcode.hasjrel: # Essentially do the same... pass  return blocks

我們得到了下面的基本單元:
 

Block 0: {'length': 12, 'conditions': {'false': 16, 'true': 12}}Block 12: {'length': 3, 'exit': True}Block 16: {'length': 12, 'conditions': {'false': 32, 'true': 28}}Block 28: {'length': 3, 'exit': True}Block 32: {'length': 17, 'exit': True}

以及單元的當前結構:
 

Basic blocks start_block_index := length := size of instructions condition := true | false | uncond -> target_index exit* := true

我們得到了控制流程圖(除了入口和隱式的退出單元),之后我們可以把它轉化成可視化的圖形:
 

def to_dot(blocks):cache = {} def get_node_id(idx, buf):if idx not in cache:cache[idx] = 'node_%d' % idxbuf.append('%s [label="Block Index %d"];' % (cache[idx], idx))return cache[idx] buffer = ['digraph CFG {']buffer.append('entry [label="CFG Entry"]; ')buffer.append('exit [label="CFG Implicit Return"]; ') for block_idx in blocks:node_id = get_node_id(block_idx, buffer)if block_idx == 0:buffer.append('entry -> %s;' % node_id)if 'conditions' in blocks[block_idx]:for cond_kind in blocks[block_idx]['conditions']:target_id = get_node_id(blocks[block_idx]['conditions'][cond_kind], buffer)buffer.append('%s -> %s [label="%s"];' % (node_id, target_id, cond_kind))if 'exit' in blocks[block_idx]:buffer.append('%s -> exit;' % node_id) buffer.append('}')return 'n'.join(buffer)

可視化的流程控制圖:

201541101827020.jpg (552×517)

為什么有這篇文章?

需要訪問Python字節碼的情況確實很少見,但是我已經遇到過幾次這種情形了。我希望,這篇文章能夠幫助那些開始研究Python逆向工程的人們。

然而現在,我正在研究Python代碼,尤其是它的字節碼。由于目前在Python中尚不存在這樣的工具(并且檢測源代碼通常會留下非常低效的裝飾器檢測代碼),這就是為什么equip會出現的原因。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 湘潭县| 肥乡县| 霍邱县| 抚远县| 宣恩县| 抚顺县| 凤山县| 敦化市| 定日县| 清水河县| 龙岩市| 长汀县| 庆云县| 阜宁县| 扎兰屯市| 西林县| 阳山县| 江山市| 葫芦岛市| 肇源县| 五指山市| 界首市| 遵义县| 忻州市| 安义县| 邓州市| 宝山区| 临邑县| 长海县| 吴堡县| 根河市| 榆中县| 宜春市| 恩施市| 抚宁县| 镇江市| 呼玛县| 承德县| 赣榆县| 周宁县| 突泉县|