亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入Python解釋器理解Python中的字節(jié)碼

 更新時(shí)間:2015年04月01日 10:25:19   作者:Romain Gaucher  
這篇文章深入Python解釋器、從其對(duì)字節(jié)碼的處理過程來詳解Python中的字節(jié)碼,需要的朋友可以參考下

我最近在參與Python字節(jié)碼相關(guān)的工作,想與大家分享一些這方面的經(jīng)驗(yàn)。更準(zhǔn)確的說,我正在參與2.6到2.7版本的CPython解釋器字節(jié)碼的工作。

Python是一門動(dòng)態(tài)語言,在命令行工具下運(yùn)行時(shí),本質(zhì)上執(zhí)行了下面的步驟:

  •     當(dāng)?shù)谝淮螆?zhí)行到一段代碼時(shí),這段代碼會(huì)被編譯(如,作為一個(gè)模塊加載,或者直接執(zhí)行)。根據(jù)操作系統(tǒng)的不同,這一步生成后綴名是pyc或者pyo的二進(jìn)制文件。
  •     解釋器讀取二進(jìn)制文件,并依次執(zhí)行指令(opcodes)。

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

探索Python二進(jìn)制文件

得到一個(gè)二進(jìn)制文件字節(jié)碼的最簡(jiǎn)單方式,是對(duì)CodeType結(jié)構(gòu)進(jìn)行解碼:
 

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

code_object包含了一個(gè)CodeType對(duì)象,它代表被加載文件的整個(gè)模塊。為了查看這個(gè)模塊的類定義、方法等的所有嵌套編碼對(duì)象(編碼對(duì)象,原文為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) # 從第一個(gè)對(duì)象開始

這個(gè)案例中,我們打印出一顆編碼對(duì)象樹,每個(gè)編碼對(duì)象是其父對(duì)象的子節(jié)點(diǎn)。對(duì)下面的代碼:
 

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

我們得到的樹形結(jié)果是:

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

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

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

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

初見字節(jié)碼

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

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

dis模塊的disassemble函數(shù)展示了是如何做到的。對(duì)我們前面例子,它輸出的結(jié)果是:

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

我們得到了:

  •     行號(hào)(當(dāng)它改變時(shí))
  •     指令的序號(hào)
  •     當(dāng)前指令的操作碼
  •     操作參數(shù)(oparg),操作碼用它來計(jì)算實(shí)際的參數(shù)。例如,對(duì)于LOAD_NAME操作碼,操作參數(shù)指向tuple co_names的索引。
  •     計(jì)算后的實(shí)際參數(shù)(圓括號(hào)內(nèi))

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

字節(jié)碼的第一部分(序號(hào)0到16),與A的類型定義有關(guān);其他的部分是我們實(shí)例化A,并打印它的代碼。

有趣的字節(jié)碼構(gòu)造

所有的操作碼都是相當(dāng)直接易懂的,但是由于下面的原因,在個(gè)別情況下會(huì)顯得奇怪:

  •     編譯器優(yōu)化
  •     解釋器優(yōu)化(因此會(huì)導(dǎo)致加入額外的操作碼)

順序變量賦值

首先,我們看看順序地對(duì)多個(gè)元素賦值,會(huì)發(fā)生什么:

(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中語句,會(huì)產(chǎn)生差別相當(dāng)大的字節(jié)碼。

第一種情況最簡(jiǎn)單,因?yàn)橘x值操作的右值(RHS)只包含常量。這種情況下,CPython會(huì)創(chuàng)建一個(gè)(1, ‘a(chǎn)') 的t uple,使用UNPACK_SEQUENCE操作碼,把兩個(gè)元素壓到棧上,并對(duì)變量a和b分別執(zhí)行STORE_FAST操作:

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

而第二種情況,則在右值引入了一個(gè)變量,因此一般情況下,會(huì)調(diào)用一條取值指令(這里簡(jiǎn)單地調(diào)用了LOAD_GLOBAL指令)。但是,編譯器不需要在棧上為這些值創(chuàng)建一個(gè)新的tuple,也不需要調(diào)用UNPACK_SEQUENCE(序號(hào)18);調(diào)用ROT_TWO就足夠了,它用來交換棧頂?shù)膬蓚€(gè)元素(雖然交換指令19和22也可以達(dá)到目的)。

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

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

25 LOAD_CONST 1 (1)
28 LOAD_CONST 3 (2)
31 LOAD_GLOBAL 0 (e)
34 ROT_THREE
35 ROT_TWO
36 STORE_FAST 0 (a)
39 STORE_FAST 1 (b)
42 STORE_FAST 2 (c)

最后一種情況是通用的處理方式,ROT_*操作看起來行不通了,編譯器創(chuàng)建了一個(gè)tuple,然后調(diào)用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 4
60 UNPACK_SEQUENCE 4
63 STORE_FAST 0 (a)
66 STORE_FAST 1 (b)
69 STORE_FAST 2 (c)
72 STORE_FAST 3 (d)

函數(shù)調(diào)用構(gòu)造

最后一組有趣的例子是關(guān)于函數(shù)調(diào)用構(gòu)造,以及創(chuàng)建調(diào)用的4個(gè)操作碼。我猜測(cè)這些操作碼的數(shù)量是為了優(yōu)化解釋器代碼,因?yàn)樗幌馢ava,有invokedynamic,invokeinterface,invokespecial,invokestatic或者invokevirtual之一。

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

在Python中,不同的CALL_*操作碼確實(shí)不存在,原因是類型系統(tǒng),靜態(tài)方法,或者特殊訪問構(gòu)造器的需求。它們都指向了Python中一個(gè)函數(shù)調(diào)用是如何確定的。從語法來看:

調(diào)用結(jié)構(gòu)允許代碼這些寫:
 

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

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

這個(gè)例子用到了調(diào)用構(gòu)造的幾乎所有特性:
? 傳遞變量參數(shù)列表(_VAR):CALL_FUNCTION_VAR, CALL_FUNCTION_VAR_KW
? 傳遞基于字典的關(guān)鍵字(_KW):CALL_FUNCTION_KW, CALL_FUNCTION_VAR_KW

字節(jié)碼是這樣的:

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調(diào)用將oparg解析為參數(shù)個(gè)數(shù)。但是,更多的信息被編碼。第一個(gè)字節(jié)(0xff掩碼)存儲(chǔ)參數(shù)的個(gè)數(shù),第二個(gè)字節(jié)((value >> 8) & 0xff)存儲(chǔ)傳遞的關(guān)鍵字參數(shù)個(gè)數(shù)。為了要計(jì)算需要從棧頂彈出的元素個(gè)數(shù),我們需要這么做:
 

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

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

對(duì)于其他的CALL_*調(diào)用,完全依賴于代碼是否使用列表或者字典傳遞參數(shù)。只需要簡(jiǎn)單的組合即可。

構(gòu)造一個(gè)極小的CFG

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

即使字節(jié)碼是一門真正的小型語言,構(gòu)造一個(gè)運(yùn)行穩(wěn)定的CFG需要大量的細(xì)節(jié)工作,遠(yuǎn)超出本博客的范圍。因此如果需要一個(gè)真實(shí)的CFG實(shí)現(xiàn),你可以看看這里equip。

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

只有少數(shù)幾個(gè)操作碼能夠執(zhí)行地址跳轉(zhuǎn)(對(duì)沒有循環(huán)和異常的情況);它們是:

    JUMP_FORWARD:在字節(jié)碼中跳轉(zhuǎn)到一個(gè)相對(duì)位置。參數(shù)是跳過的字節(jié)數(shù)。
    JUMP_IF_FALSE_OR_POP,JUMP_IF_TRUE_OR_POP,JUMP_ABSOLUTE,POP_JUMP_IF_FALSE,以及POP_JUMP_IF_TRUE:參數(shù)都是字節(jié)碼中的絕對(duì)地址。

為一個(gè)函數(shù)夠造CFG,意味著要?jiǎng)?chuàng)建基本的單元(不包含條件判斷的操作碼序列——除非有異常發(fā)生),并且把它們與條件和分支連在一起,構(gòu)成一個(gè)圖。在我們的例子中,我們只有True、False和無條件分支。

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

def factorial(n):
if n &lt;= 1:
return 1
elif n == 2:
return 2
return n * factorial(n - 1)

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

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

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

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

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

    POP_JUMP_IF_FALSE:跳轉(zhuǎn)到絕對(duì)地址16和32;
    RETURN_VALUE:從棧頂彈出一個(gè)元素,并返回。

提取基本單元很簡(jiǎn)單,因?yàn)槲覀冎魂P(guān)心那些改變控制流程的指令。在我們的例子中,我們沒有遇到強(qiáng)制跳轉(zhuǎn)指令,如JUMP_FORWARD或JUMP_ABSOLUTE。

提取這類結(jié)構(gòu)的代碼示例:

import opcode
RETURN_VALUE = 83
JUMP_FORWARD, JUMP_ABSOLUTE = 110, 113
FALSE_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}

以及單元的當(dāng)前結(jié)構(gòu):
 

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

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

def to_dot(blocks):
cache = {}
 
def get_node_id(idx, buf):
if idx not in cache:
cache[idx] = 'node_%d' % idx
buf.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 -&gt; %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 -&gt; %s [label="%s"];' % (node_id, target_id, cond_kind))
if 'exit' in blocks[block_idx]:
buffer.append('%s -&gt; exit;' % node_id)
 
buffer.append('}')
return 'n'.join(buffer)

可視化的流程控制圖:

201541101827020.jpg (552×517)

為什么有這篇文章?

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

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

相關(guān)文章

  • Python詞云的正確實(shí)現(xiàn)方法實(shí)例

    Python詞云的正確實(shí)現(xiàn)方法實(shí)例

    這篇文章主要給大家介紹了關(guān)于Python詞云的正確實(shí)現(xiàn)方法的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • python使用pandas實(shí)現(xiàn)數(shù)據(jù)分割實(shí)例代碼

    python使用pandas實(shí)現(xiàn)數(shù)據(jù)分割實(shí)例代碼

    這篇文章主要介紹了python使用pandas實(shí)現(xiàn)數(shù)據(jù)分割實(shí)例代碼,介紹了使用pandas實(shí)現(xiàn)對(duì)dataframe格式的數(shù)據(jù)分割成時(shí)間跨度相等的數(shù)據(jù)塊,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2018-01-01
  • Python如何給你的程序做性能測(cè)試

    Python如何給你的程序做性能測(cè)試

    這篇文章主要介紹了Python如何給你的程序做性能測(cè)試,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-07-07
  • python程序中斷然后接著中斷代碼繼續(xù)運(yùn)行問題

    python程序中斷然后接著中斷代碼繼續(xù)運(yùn)行問題

    這篇文章主要介紹了python程序中斷然后接著中斷代碼繼續(xù)運(yùn)行問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-02-02
  • Python math庫 ln(x)運(yùn)算的實(shí)現(xiàn)及原理

    Python math庫 ln(x)運(yùn)算的實(shí)現(xiàn)及原理

    這篇文章主要介紹了Python math庫 ln(x)運(yùn)算的實(shí)現(xiàn)及原理,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-07-07
  • Python計(jì)算多幅圖像柵格值的平均值

    Python計(jì)算多幅圖像柵格值的平均值

    這篇文章主要為大家詳細(xì)介紹了Python計(jì)算多幅圖像柵格值的平均值,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-06-06
  • Python貪心算法Greedy Algorithm解決案例小結(jié)

    Python貪心算法Greedy Algorithm解決案例小結(jié)

    這篇文章主要為大家介紹了Python貪心算法Greedy Algorithm解決案例小結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-06-06
  • 簡(jiǎn)單談?wù)凱ython中的json與pickle

    簡(jiǎn)單談?wù)凱ython中的json與pickle

    下面小編就為大家?guī)硪黄?jiǎn)單談?wù)凱ython中的json與pickle。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-07-07
  • 解決Tensorflow安裝成功,但在導(dǎo)入時(shí)報(bào)錯(cuò)的問題

    解決Tensorflow安裝成功,但在導(dǎo)入時(shí)報(bào)錯(cuò)的問題

    今天小編就為大家分享一篇解決Tensorflow安裝成功,但在導(dǎo)入時(shí)報(bào)錯(cuò)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-06-06
  • python實(shí)現(xiàn)文法左遞歸的消除方法

    python實(shí)現(xiàn)文法左遞歸的消除方法

    這篇文章主要介紹了python實(shí)現(xiàn)文法左遞歸的消除的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05

最新評(píng)論