日韩久久久精品,亚洲精品久久久久久久久久久,亚洲欧美一区二区三区国产精品 ,一区二区福利

python反編譯學習之字節(jié)碼詳解

系統(tǒng) 1675 0

前言

如果你曾經(jīng)寫過或者用過 Python,你可能已經(jīng)習慣了看到 Python 源代碼文件;它們的名稱以.Py 結(jié)尾。你可能還見過另一種類型的文件是 .pyc 結(jié)尾的,它們就是 Python “字節(jié)碼”文件。(在 Python3 的時候這個 .pyc 后綴的文件不太好找了,它在一個名為__pycache__的子目錄下面。).pyc文件可以防止Python每次運行時都重新解析源代碼,該文件大大節(jié)省了時間。

Python是如何工作的

Python 通常被描述為一種解釋語言,在這種語言中,你的源代碼在程序運行時被翻譯成CPU指令,但這只是說對了部分。和許多解釋型語言一樣,Python 實際上將源代碼編譯為虛擬機的一組指令,Python 解釋器就是該虛擬機的實現(xiàn)。其中這種中間格式稱為“字節(jié)碼”。

因此,Python留下的這些.pyc文件,是為了讓運行的速快變得 “更快”,或者是針對你的源代碼的”優(yōu)化“的版本;它們是 Python 虛擬機上運行的字節(jié)碼指令。

Python 虛擬機內(nèi)幕

CPython使用基于堆棧的虛擬機。也就是說,它完全圍繞堆棧數(shù)據(jù)結(jié)構(gòu)(你可以將項目“推”到結(jié)構(gòu)的“頂部”,或者將項目“彈出”到“頂部”)。

CPython 使用三種類型的棧:

1.調(diào)用堆棧。這是運行中的Python程序的主要結(jié)構(gòu)。對于每個當前活動的函數(shù)調(diào)用,它都有一個項目一“幀”,堆棧的底部是程序的入口點。每次函數(shù)調(diào)用都會將新的幀推到調(diào)用堆棧上,每次函數(shù)調(diào)用返回時,它的幀都會彈出

2.在每一幀中,都有一個評估堆棧(也稱為數(shù)據(jù)堆棧)。這個堆棧是執(zhí)行 Python 函數(shù)的地方,執(zhí)行Python代碼主要包括將東西推到這個堆棧上,操縱它們,然后將它們彈出。

3.同樣在每一幀中,都有一個塊堆棧。Python使用它來跟蹤某些類型的控制結(jié)構(gòu):循環(huán)、try /except塊,以及 with 塊都會導致條目被推送到塊堆棧上,每當退出這些結(jié)構(gòu)之一時,塊堆棧就會彈出。這有助于Python知道在任何給定時刻哪些塊是活動的,例如,continue或break語句可以影響正確的塊。

大多數(shù) Python 字節(jié)碼指令操作的是當前調(diào)用棧幀的計算棧,雖然,還有一些指令可以做其它的事情(比如跳轉(zhuǎn)到指定指令,或者操作塊棧)。

為了更好地理解,假設我們有一些調(diào)用函數(shù)的代碼,比如這個:

            
my_function(my_variable,2)。
          

Python 將轉(zhuǎn)換為一系列字節(jié)碼指令:

1.一個LOAD_NAME指令,用于查找函數(shù)對象 my_function,并將其推送到計算棧的頂部

2.另一個 LOAD_NAME 指令去查找變量 my_variable,并將其推送到計算棧的頂部

3.一個 LOAD_CONST 指令將一個整數(shù) 2 推送到計算棧的頂部

4.一個 CALL_FUNCTION 指令

CALL_FUNCTION 指令有2個參數(shù),它表示 Python 需要在堆棧頂部彈出兩個位置參數(shù); 然后函數(shù)將在它上面進行調(diào)用,并且它也同時被彈出(關(guān)鍵字參數(shù)的函數(shù),使用指令-CALL_FUNCTION_KW-類似的操作,并配合使用第三條指令CALL_FUNCTION_EX,它適用于函數(shù)調(diào)用涉及到參數(shù)使用 * 或 ** 操作符的情況)
一旦 Python 具備了這些,它將在調(diào)用堆棧上分配一個新的幀,填充到函數(shù)調(diào)用的本地變量,然后運行該幀內(nèi)的 my_function 的字節(jié)碼。一旦運行完成,幀將從調(diào)用堆棧中彈出,在原始幀中,my_function 的返回值將被推入到計算棧的頂部。

我們知道了這個東西了,也知道字節(jié)碼了文件了,但是如何去使用字節(jié)碼呢?ok不知道也沒關(guān)系,接下來的時間我們所有的話題都將圍繞字節(jié)碼,在python有一個模塊可以通過反編譯Python代碼來生成字節(jié)碼這個模塊就是今天要說的--dis模塊。

dis模塊的使用

dis模塊包括一些用于處理 Python 字節(jié)碼的函數(shù),可以將字節(jié)碼“反匯編”為更便于人閱讀的形式。查看解釋器運行的字節(jié)碼還有助于優(yōu)化代碼。這個模塊對于查找多線程中的競態(tài)條件也很有用,因為可以用它評估代碼中哪一點線程控制可能切換。參考源碼Include/opcode.h,可以找到字節(jié)碼的正式列表。詳細可以看官方文檔。注意不同版本的python生成的字節(jié)碼內(nèi)容可能不一樣,這里我用的Python 3.8.

訪問和理解字節(jié)碼

輸入如下內(nèi)容,然后運行它:

            
def hello()
 print("Hello, World!")
import dis
dis.dis(hello)
          

函數(shù) dis.dis() 將反匯編一個函數(shù)、方法、類、模塊、編譯過的 Python 代碼對象、或者字符串包含的源代碼,以及顯示出一個人類可讀的版本。dis 模塊中另一個方便的功能是 distb()。你可以給它傳遞一個 Python 追溯對象,或者在發(fā)生預期外情況時調(diào)用它,然后它將在發(fā)生預期外情況時反匯編調(diào)用棧上最頂端的函數(shù),并顯示它的字節(jié)碼,以及插入一個指向到引發(fā)意外情況的指令的指針。

它也可以用于查看 Python 為每個函數(shù)構(gòu)建的編譯后的代碼對象,因為運行一個函數(shù)將會用到這些代碼對象的屬性。這里有一個查看 hello() 函數(shù)的示例:

            
>>> hello.__code__

            
              ", line 1>
>>> hello.__code__.co_consts
(None, 'Hello, World!')
>>> hello.__code__.co_varnames
()
>>> hello.__code__.co_names
('print',)
            
          

代碼對象在函數(shù)中可以以屬性 __code__ 來訪問,并且攜帶了一些重要的屬性:

co_consts 是存在于函數(shù)體內(nèi)的任意實數(shù)的元組

co_varnames 是函數(shù)體內(nèi)使用的包含任意本地變量名字的元組

co_names 是在函數(shù)體內(nèi)引用的任意非本地名字的元組

許多字節(jié)碼指令--尤其是那些推入到棧中的加載值,或者在變量和屬性中的存儲值--在這些元組中的索引作為它們參數(shù)。

因此,現(xiàn)在我們能夠理解 hello() 函數(shù)中所列出的字節(jié)碼:

1、LOAD_GLOBAL 0:告訴 Python 通過 co_names (它是 print 函數(shù))的索引 0 上的名字去查找它指向的全局對象,然后將它推入到計算棧

2、LOAD_CONST 1:帶入 co_consts 在索引 1 上的字面值,并將它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,因為 Python 函數(shù)調(diào)用有一個隱式的返回值 None,如果沒有顯式的返回表達式,就返回這個隱式的值 )。

3、CALL_FUNCTION 1:告訴 Python 去調(diào)用一個函數(shù);它需要從棧中彈出一個位置參數(shù),然后,新的棧頂將被函數(shù)調(diào)用。

“原始的” 字節(jié)碼--是非人類可讀格式的字節(jié)--也可以在代碼對象上作為 co_code 屬性可用。如果你有興趣嘗試手工反匯編一個函數(shù)時,你可以從它們的十進制字節(jié)值中,使用列出 dis.opname 的方式去查看字節(jié)碼指令的名字。

基本反匯編

函數(shù)dis()可以打印 Python 源代碼(模塊、類、方法、函數(shù)或代碼對象)的反匯編表示。可以通過從命令行運行 dis 來反匯編 dis_simple.py 之類的模塊。

            
dis_simple.py
#!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}
          

輸出按列組織,包含原始源代碼行號,代碼對象中的指令地址,操作碼名稱以及傳遞給操作碼的任何參數(shù)。
對于簡單的代碼我們可以通過命令行的形式執(zhí)行下面的命令:

            
python3 -m dis dis_simple.py
          

輸出

?1????? 0 LOAD_CONST??????? 0 ('a')
?????? 2 LOAD_CONST??????? 1 (1)
?????? 4 BUILD_MAP??????? 1
?????? 6 STORE_NAME??????? 0 (my_dict)
?????? 8 LOAD_CONST??????? 2 (None)
?????? 10 RETURN_VALUE

在這里源代碼轉(zhuǎn)換為4個不同的操作來創(chuàng)建和填充字典,然后將結(jié)果保存到一個局部變量。

首先解釋每一行各列參數(shù)的含義:

以第一條指令為例:

第一列 數(shù)字(1)表示對應源代碼的行數(shù)。

第二列(可選)指示當前執(zhí)行的指令(例如,當字節(jié)碼來自幀對象時)【這個例子沒有】

第三列 一個標簽,表示從之前的指令到此可能的JUMP 【這個例子沒有】

第四列 數(shù)字是字節(jié)碼中對應于字節(jié)索引的地址(這些是2的倍數(shù),因為Python 3.6每條指令使用2個字節(jié),而在以前的版本中可能會有所不同)指令LOAD_CONST在0位置。

第五列 指令本身對應的人類可讀的名字這里是"LOAD_CONST"

第六列 Python內(nèi)部用于獲取某些常量或變量,管理堆棧,跳轉(zhuǎn)到特定指令等的指令的參數(shù)(如果有的話)。

第七列 計算后的實際參數(shù)。

然后讓我們看看這個過程:

由于 Python 解釋器是基于棧的,所以前幾步是用LOAD_CONST將常量按正確順序放入到棧中,然后使用 BUILD_MAP 彈出要增加到字典的新鍵和值。用 STORE_NAME 將所得到的dict對象綁定名為my_dict.

反匯編函數(shù)

需要注意的是上面的命令行反編譯的形式,不能自動的遞歸反編譯函數(shù),所以我們要使用在文件中導入dis的模式進行反編譯,就像下面這樣。

            
#dis_function.py
def f(*args):
 nargs = len(args)
 print(nargs, args)

if __name__ == '__main__':
 import dis
 dis.dis(f)
          

運行命令

            
python3 dis_function.py
          

然后得到以下結(jié)果

? 2?????????? 0 LOAD_GLOBAL????????????? 0 (len)
????????????? 2 LOAD_FAST??????????????? 0 (args)
????????????? 4 CALL_FUNCTION??????????? 1
????????????? 6 STORE_FAST?????????????? 1 (nargs)

? 3?????????? 8 LOAD_GLOBAL????????????? 1 (print)
???????????? 10 LOAD_FAST??????????????? 1 (nargs)
???????????? 12 LOAD_FAST??????????????? 0 (args)
???????????? 14 CALL_FUNCTION??????????? 2
???????????? 16 POP_TOP
???????????? 18 LOAD_CONST?????????????? 0 (None)
???????????? 20 RETURN_VALUE

要查看函數(shù)的內(nèi)部,必須把函數(shù)傳遞到dis().因為這里打印的是函數(shù)內(nèi)部的東西,所以沒有顯示函數(shù)的在外層的行編號,而是從2開始的。

下面解析下每一行指令的含義:

1、LOAD_GLOBAL 用來加載全局變量,包括指定函數(shù)名,類名,模塊名等全局符號,這里是len函數(shù),LOAD_FAST 一般加載局部變量的值,也就是讀取值,用于計算或者函數(shù)調(diào)用傳參等,這里就是傳入?yún)?shù)args。

2、一般是先指定要調(diào)用的函數(shù),然后壓參數(shù),最后通過 CALL_FUNCTION 調(diào)用。

3、STORE_FAST 保存值到局部變量。也就是把結(jié)果賦值給 STORE_FAST。

4、下面的print因為2個參數(shù)所以LOAD_FAST了2次,POP_TOP刪除堆棧頂部(TOS)項。LOAD_CONST加載const變量,比如數(shù)值、字符串等等,這里因為是print所以值為None。

5、最后通過RETURN_VALUE來確定函數(shù)結(jié)尾。

要打印一個函數(shù)的總結(jié)信息我們可以使用dis的show_code的方法,它包含使用的參數(shù)和名的相關(guān)信息,show_code的參數(shù)就是這個函數(shù)對象,代碼如下:

            
def f(*args):
 nargs = len(args)
 print(nargs, args)

if __name__ == '__main__':
 import dis
 dis.show_code(f)
          

運行之后,結(jié)果如下

Name:????????????? f
Filename:????????? dis_function_showcode.py
Argument count:??? 0
Kw-only arguments: 0
Number of locals:? 2
Stack size:??????? 3
Flags:???????????? OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
?? 0: None
Names:
?? 0: len
?? 1: print
Variable names:
?? 0: args
?? 1: nargs

可以看到返回的內(nèi)容有函數(shù),方法,參數(shù)等信息。

反匯編類

上面我們知道了如何反匯編一個函數(shù)的內(nèi)部,同樣的我們也可以用類似的方法反匯編一個類。

我們看一個例子:

            
import dis

class MyObject:
 """Example for dis."""

 CLASS_ATTRIBUTE = 'some value'

 def __str__(self):
  return 'MyObject({})'.format(self.name)

 def __init__(self, name):
  self.name = name

if __name__ == '__main__':
 dis.dis(MyObject)
          

運行之和得到如下結(jié)果

Disassembly of __init__:
?12?????????? 0 LOAD_FAST??????????????? 1 (name)
????????????? 2 LOAD_FAST??????????????? 0 (self)
????????????? 4 STORE_ATTR?????????????? 0 (name)
????????????? 6 LOAD_CONST?????????????? 0 (None)
????????????? 8 RETURN_VALUE

Disassembly of __str__:
? 9?????????? 0 LOAD_CONST?????????????? 1 ('MyObject({})')
????????????? 2 LOAD_METHOD????????????? 0 (format)
????????????? 4 LOAD_FAST??????????????? 0 (self)
????????????? 6 LOAD_ATTR??????????????? 1 (name)
????????????? 8 CALL_METHOD????????????? 1
???????????? 10 RETURN_VALUE

從整體內(nèi)容來看,結(jié)果分為了兩部分Disassembly of __init__和Disassembly of __str__,Disassembly就是反匯編的意思。

首先分析__init__部分:

1、然后需要注意的一點是,方法是按照字母的順序列出的,所以在部分,先看到name再看到self,但是他們都是 LOAD_FAST。

2、STORE_ATTR實現(xiàn)self.name = name。

3、然后LOAD_CONST一個None和RETURN_VALUE標志著函數(shù)結(jié)束。

接下來分析__str__部分:

1、LOAD_CONST將'MyObject({})'加載到棧

2、然后通過 LOAD_METHOD 調(diào)用字符串format方法。這個方法是Python3.7新加入的。

3、LOAD_FAST 也就是到了self了。

4、LOAD_ATTR 一般是調(diào)用某個對象的方法時。這里就是self.name的.name操作

5、CALL_METHOD 是 python3.7 新增加的內(nèi)容,這里是執(zhí)行方法。

6、RETURN_VALUE表示函數(shù)的結(jié)束。

上面字符串的拼接我們用了format,之前我一直推薦用f-string,下面就讓我們通過字節(jié)碼來分析,為什么f-string比format要高快。

代碼其他代碼不變,把return改成以下內(nèi)容:

            
return f'MyObject({self.name})'
          

再次執(zhí)行,下面我們只看__str__函數(shù)的部分。

Disassembly of __str__: 9 0 LOAD_CONST 1 ('MyObject(') 2 LOAD_FAST 0 (self) 4 LOAD_ATTR 0 (name) 6 FORMAT_VALUE 0 8 LOAD_CONST 2 (')') 10 BUILD_STRING 3 12 RETURN_VALUE 對比發(fā)現(xiàn)我們這里沒有了調(diào)用方法的操作LOAD_METHOD,取而代之使用了用于實現(xiàn)fstring的FORMAT_VALUE指令。之后通過BUILD_STRING連接堆棧中的計數(shù)字符串并將結(jié)果字符串推入堆棧.為什么format慢呢, python中的函數(shù)調(diào)用具有相當大的開銷。 當使用str.format()時,CALL_METHOD 中花費的額外時間是導致str.format()比fstring慢得多。

使用反匯編調(diào)試

調(diào)試一個異常時,有時要查看哪個字節(jié)碼帶來了問題。這個時候就很有用了,要對一個錯誤周圍的代碼反匯編,有多種方法。第一種策略是在交互解釋器中使用dis()報告最后一個異常。
如果沒有向dis()傳入任何參數(shù),那么它會查找一個異常,并顯示導致這個異常的棧頂元素的反匯編效果。

命令行上使用

打開我的命令行執(zhí)行如下操作:

            
 chennan@chennandeMacBook-Pro-2 ?? ~ ?? python3
Python 3.8.0a3 (v3.8.0a3:9a448855b5, Mar 25 2019, 17:05:20)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
 File "
            
              ", line 1, in 
              
                
NameError: name 'i' is not defined
>>> dis.dis()
 1 -->  0 LOAD_NAME    0 (i)
    2 LOAD_CONST    0 (4)
    4 BINARY_ADD
    6 STORE_NAME    0 (i)
    8 LOAD_CONST    1 (None)
    10 RETURN_VALUE
>>>
              
            
          

行號后面的-->就是導致錯誤的操作碼,一個LOAD_NAME指令,由于沒有定義變量i,所以無法將與這個名關(guān)聯(lián)的值加載到棧中。

代碼中使用distb

程序還可以打印一個活動的traceback的有關(guān)信息,將它傳遞到distb()方法。

下面的程序中有個DiviedByZero異常;但是這個公式有兩個除法,所以不清楚是哪一部分出錯,此時我們就可以使用下面的方法:

            
dis_traceback.py

i = 1
j = 0
k = 3

try:
 result = k * (i / j) + (i / k)
except Exception:
 import dis
 import sys
 exc_type, exc_value, exc_tb = sys.exc_info()
 dis.distb(exc_tb)
          

運行之后輸出

? 1?????????? 0 LOAD_CONST?????????????? 0 (1)
????????????? 2 STORE_NAME?????????????? 0 (i)

? 2?????????? 4 LOAD_CONST?????????????? 1 (0)
????????????? 6 STORE_NAME?????????????? 1 (j)

? 3?????????? 8 LOAD_CONST?????????????? 2 (3)
???????????? 10 STORE_NAME?????????????? 2 (k)

? 5????????? 12 SETUP_FINALLY?????????? 24 (to 38)

? 6????????? 14 LOAD_NAME??????????????? 2 (k)
???????????? 16 LOAD_NAME??????????????? 0 (i)
???????????? 18 LOAD_NAME??????????????? 1 (j)
??? -->????? 20 BINARY_TRUE_DIVIDE
???????????? 22 BINARY_MULTIPLY
???????????? 24 LOAD_NAME??????????????? 0 (i)
???????????? 26 LOAD_NAME??????????????? 2 (k)
???????????? 28 BINARY_TRUE_DIVIDE
...
??????? >>?? 96 END_FINALLY
??????? >>?? 98 LOAD_CONST?????????????? 3 (None)
??????????? 100 RETURN_VALUE

結(jié)果反映的字節(jié)碼很長我們不用全看了,看最開始出現(xiàn)--> 就可以知道錯誤的位置了。

其中SETUP_FINALLY 字節(jié)碼的含義是將try塊從try-except子句推入塊堆棧。

這里可以看出將LOAD_NAME 將j壓入棧之后就報錯了。所以可以推斷出在(i/j)就出錯了。

參考資料

  • https://docs.python.org/zh-cn/3.7/library/dis.html#opcode-STORE_FAST
  • https://opensource.com/article/18/4/introduction-python-bytecode
  • https://hackernoon.com/a-closer-look-at-how-python-f-strings-work-f197736b3bdb

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對腳本之家的支持。


更多文章、技術(shù)交流、商務合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 潼关县| 历史| 霞浦县| 芜湖市| 望江县| 宜章县| 遵义县| 桃江县| 青田县| 龙泉市| 且末县| 德阳市| 扎赉特旗| 延安市| 彰武县| 荣昌县| 密云县| 梅州市| 东乌| 云和县| 偏关县| 广平县| 翁牛特旗| 嘉祥县| 揭阳市| 新蔡县| 武穴市| 互助| 潼关县| 牟定县| 西平县| 徐水县| 偏关县| 三原县| 汝阳县| 成安县| 万安县| 同江市| 怀集县| 合阳县| 浦东新区|