死磕python位元組碼-手工還原python原始碼

Editor發表於2018-09-21

0x1.前言

Python 程式碼先被編譯為位元組碼後,再由Python虛擬機器來執行位元組碼, Python的位元組碼是一種類似彙編指令的中間語言, 一個Python語句會對應若干位元組碼指令,虛擬機器一條一條執行位元組碼指令, 從而完成程式執行。
Python dis 模組支援對Python程式碼進行反彙編, 生成位元組碼指令。

dis.dis()將CPython位元組碼轉為可讀的虛擬碼(類似於彙編程式碼)。結構如下:

7          0LOAD_CONST              1(0)

3STORE_FAST              1(local1)

8          6LOAD_CONST              2(101)

9STORE_GLOBAL            0(global1)

9         12LOAD_FAST               1(local1)

15PRINT_ITEM

16LOAD_FAST               0(arg1)

19PRINT_ITEM

20LOAD_GLOBAL             0(global1)

23PRINT_ITEM

24PRINT_NEWLINE

25LOAD_CONST              0(None)

28RETURN_VALUE

其實就是這樣的結構:

原始碼行號 | 指令在函式中的偏移 | 指令符號 | 指令引數 | 實際引數值


0x2.變數


1.const


LOAD_CONST載入const變數,比如數值、字串等等,一般用於傳給函式的引數:


55      12LOAD_GLOBAL             1(test)

15LOAD_FAST               0(2)#讀取2

18LOAD_CONST              1('output')

21CALL_FUNCTION           2

轉為python程式碼就是:


test(2,'output')

2.區域性變數


LOAD_FAST一般載入區域性變數的值,也就是讀取值,用於計算或者函式呼叫傳參等。
STORE_FAST一般用於儲存值到區域性變數。

61         77LOAD_FAST               0(n)

80LOAD_FAST               3(p)

83INPLACE_DIVIDE

84STORE_FAST              0(n)

這段bytecode轉為python就是:


n=n/p

函式的形參也是區域性變數,如何區分出是函式形參還是其他區域性變數呢?

形參沒有初始化,也就是從函式開始到LOAD_FAST該變數的位置,如果沒有看到STORE_FAST,那麼該變數就是函式形參。

而其他區域性變數在使用之前肯定會使用STORE_FAST進行初始化。

具體看下面的例項:

4          0LOAD_CONST              1(0)

3STORE_FAST              1(local1)

5          6LOAD_FAST               1(local1)

9PRINT_ITEM

10LOAD_FAST               0(arg1)

13PRINT_ITEM

14PRINT_NEWLINE

15LOAD_CONST              0(None)

18RETURN_VALUE

對應的python程式碼如下,對比一下就一目瞭然。

deftest(arg1):

local1=0

printlocal1, arg1

3.全域性變數

LOAD_GLOBAL用來載入全域性變數,包括指定函式名,類名,模組名等全域性符號。

STORE_GLOBAL用來給全域性變數賦值。

8          6LOAD_CONST              2(101)

9STORE_GLOBAL            0(global1)

20LOAD_GLOBAL             0(global1)

23PRINT_ITEM

對應的python程式碼

deftest():

globalglobal1

global1=101

printglobal


0x3.常用資料型別

1.list

BUILD_LIST用於建立一個list結構。

13          0LOAD_CONST              1(1)

3LOAD_CONST              2(2)

6BUILD_LIST              2

9STORE_FAST              0(k)

對應python程式碼是:

k=[1,2]

另外再看看一種常見的建立list的方式如下:

[xforxinxlistifx!=0]

一個例項bytecode如下:

22        235BUILD_LIST              0//建立list,為賦值給某變數,這種時候一般都是語法糖結構了

238LOAD_FAST               3(sieve)

241GET_ITER

>> 242FOR_ITER               24(to269)

245STORE_FAST              4(x)

248LOAD_FAST               4(x)

251LOAD_CONST              2(0)

254COMPARE_OP              3(!=)

257POP_JUMP_IF_FALSE     242//不滿足條件contine

260LOAD_FAST               4(x)//讀取滿足條件的x

263LIST_APPEND             2//把每個滿足條件的x存入list

266JUMP_ABSOLUTE         242

>> 269RETURN_VALUE

轉為python程式碼是:

[forxinsieveifx !=0]

2.dict

BUILD_MAP用於建立一個空的dict。STORE_MAP用於初始化dict的內容。

13          0BUILD_MAP               1

3LOAD_CONST              1(1)

6LOAD_CONST              2('a')

9STORE_MAP

10STORE_FAST              0(k)

對應的python程式碼是:

k={'a':1}


再看看修改dict的bytecode:

14         13LOAD_CONST              3(2)

16LOAD_FAST               0(k)

19LOAD_CONST              4('b')

22STORE_SUBSCR

對應的python程式碼是:

k['b']=2

3.slice

BUILD_SLICE用於建立slice。對於list、元組、字串都可以使用slice的方式進行訪問。

但是要注意BUILD_SLICE用於[x:y:z]這種型別的slice,結合BINARY_SUBSCR讀取slice的值,結合STORE_SUBSCR用於修改slice的值。

另外SLICE+n用於[a:b]型別的訪問,STORE_SLICE+n用於[a:b]型別的修改,其中n表示如下:

SLICE+0()

Implements TOS=TOS[:].

SLICE+1()

Implements TOS=TOS1[TOS:].

SLICE+2()

Implements TOS=TOS1[:TOS].

SLICE+3()

Implements TOS=TOS2[TOS1:TOS].

下面看具體例項:

13           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 BUILD_LIST               3
             12 STORE_FAST               0 (k1) //k1 = [1, 2, 3]
 
 14          15 LOAD_CONST               4 (10)
             18 BUILD_LIST               1
             21 LOAD_FAST                0 (k1)
             24 LOAD_CONST               5 (0)
             27 LOAD_CONST               1 (1)
             30 LOAD_CONST               1 (1)
             33 BUILD_SLICE              3
             36 STORE_SUBSCR                    //k1[0:1:1] = [10]
 
 15          37 LOAD_CONST               6 (11)
             40 BUILD_LIST               1
             43 LOAD_FAST                0 (k1)
             46 LOAD_CONST               1 (1)
             49 LOAD_CONST               2 (2)
             52 STORE_SLICE+3                   //k1[1:2] = [11]
 
 16          53 LOAD_FAST                0 (k1)
             56 LOAD_CONST               1 (1)
             59 LOAD_CONST               2 (2)
             62 SLICE+3
             63 STORE_FAST               1 (a)  //a = k1[1:2]
 
 17          66 LOAD_FAST                0 (k1)
             69 LOAD_CONST               5 (0)
             72 LOAD_CONST               1 (1)
             75 LOAD_CONST               1 (1)
             78 BUILD_SLICE              3
             81 BINARY_SUBSCR
             82 STORE_FAST               2 (b) //b = k1[0:1:1]

0x4.迴圈

SETUP_LOOP用於開始一個迴圈。SETUP_LOOP 26 (to 35)中35表示迴圈退出點。

while迴圈

23          0LOAD_CONST              1(0)

3STORE_FAST              0(i)//i=0

24          6SETUP_LOOP             26(to35)

>>   9LOAD_FAST               0(i)//迴圈起點

12LOAD_CONST              2(10)

15COMPARE_OP              0(<)

18POP_JUMP_IF_FALSE      34    //whilei <10:

25         21LOAD_FAST               0(i)

24LOAD_CONST              3(1)

27INPLACE_ADD                    

28STORE_FAST              0(i)//i+=1

31JUMP_ABSOLUTE           9   //回到迴圈起點

>>  34POP_BLOCK

>>  35LOAD_CONST              0(None)

對應python程式碼是:

i=0

whilei <10:

i+=1

for in結構

238LOAD_FAST               3(sieve)#sieve是個list

241GET_ITER                   //開始迭代sieve

>> 242FOR_ITER               24(to269)//繼續iter下一個x

245STORE_FAST              4(x)

...

266JUMP_ABSOLUTE         242//迴圈

這是典型的for+in結構,轉為python程式碼就是:

forxinsieve:

0x5.if

POP_JUMP_IF_FALSE和JUMP_FORWARD一般用於分支判斷跳轉。POP_JUMP_IF_FALSE表示條件結果為FALSE就跳轉到目標偏移指令。JUMP_FORWARD直接跳轉到目標偏移指令。

23          0LOAD_CONST              1(0)

3STORE_FAST              0(i)//i=0

24          6LOAD_FAST               0(i)

9LOAD_CONST              2(5)

12COMPARE_OP              0(<)

15POP_JUMP_IF_FALSE      26

25         18LOAD_CONST              3('i < 5')

21PRINT_ITEM

22PRINT_NEWLINE

23JUMP_FORWARD           25(to51)

26    >>  26LOAD_FAST               0(i)

29LOAD_CONST              2(5)

32COMPARE_OP              4(>)

35POP_JUMP_IF_FALSE      46

27         38LOAD_CONST              4('i > 5')

41PRINT_ITEM

42PRINT_NEWLINE

43JUMP_FORWARD            5(to51)

29    >>  46LOAD_CONST              5('i = 5')

49PRINT_ITEM

50PRINT_NEWLINE

>>  51LOAD_CONST              0(None)

轉為python程式碼是:

i=0

ifi <5:

print'i < 5'

elifi >5:

print'i > 5'

else:

print'i = 5'

0x6.分辨函式

1.函式範圍

前面介紹第二列表示指令在函式中的偏移地址,所以看到0就是函式開始,下一個0前一條指令就是函式結束位置,當然也可以通過RETURN_VALUE來確定函式結尾

54        0LOAD_FAST               1(plist)//函式開始

3LOAD_CONST              0(None)

6COMPARE_OP              2(==)

9POP_JUMP_IF_FALSE       33

55        ...

67    >> 139LOAD_FAST             2(fs)

142RETURN_VALUE

70        0LOAD_CONST              1('FLAG')//另一個函式開始

3STORE_FAST              0(flag)

2.函式呼叫

函式呼叫類似於push+call的彙編結構,壓棧引數從左到右依次壓入(當然不是push,而是讀取指令LOAD_xxxx來指定引數)。

函式名一般通過LOAD_GLOBAL指令指定,如果是模組函式或者類成員函式通過LOAD_GLOBAL+LOAD_ATTR來指定。

先指定要呼叫的函式,然後壓引數,最後通過CALL_FUNCTION呼叫。

CALL_FUNCTION後面的值表示有幾個引數。

支援巢狀呼叫:

6          0LOAD_GLOBAL             0(int)//int函式

3LOAD_GLOBAL             1(math)//math模組

6LOAD_ATTR               2(sqrt)//sqrt函式

9LOAD_FAST               0(n)//引數

12CALL_FUNCTION           1

15CALL_FUNCTION           1

18STORE_FAST              2(nroot)

這段bytecode轉換成python程式碼就是:

nroot=int(math.sqrt(n))//其中n是一個區域性變數或者函式引數,具體看上下文

0x7.其他指令

其他常見指令,一看就明白,就不具體分析了,更多詳細內容請看官方文件

INPLACE_POWER()
Implements in-place TOS = TOS1 ** TOS.
 
INPLACE_MULTIPLY()
Implements in-place TOS = TOS1 * TOS.
 
INPLACE_DIVIDE()
Implements in-place TOS = TOS1 / TOS when from __future__ import division is not in effect.
 
INPLACE_FLOOR_DIVIDE()
Implements in-place TOS = TOS1 // TOS.
 
INPLACE_TRUE_DIVIDE()
Implements in-place TOS = TOS1 / TOS when from __future__ import division is in effect.
 
INPLACE_MODULO()
Implements in-place TOS = TOS1 % TOS.
 
INPLACE_ADD()
Implements in-place TOS = TOS1 + TOS.
 
INPLACE_SUBTRACT()
Implements in-place TOS = TOS1 - TOS.
 
INPLACE_LSHIFT()
Implements in-place TOS = TOS1 << TOS.
 
INPLACE_RSHIFT()
Implements in-place TOS = TOS1 >> TOS.
 
INPLACE_AND()
Implements in-place TOS = TOS1 & TOS.
 
INPLACE_XOR()
Implements in-place TOS = TOS1 ^ TOS.
 
INPLACE_OR()
Implements in-place TOS = TOS1 | TOS.

基礎運算還有一套對應的BINARY_xxxx指令,兩者區別很簡單。

i+=1//使用INPLACE_xxx

i=i+1//使用BINARY_xxxx

參考資料

  1. python dis官方文件
  2. google搜尋dis指令
  3. https://github.com/vstinner/bytecode
  4. https://blog.hakril.net/articles/2-understanding-python-execution-tracer.html
  5. A Python Interpreter Written in Python
  6. https://blog.csdn.net/qs9816/article/details/51661659
  7. https://github.com/Mysterie/uncompyle2



原文作者:Angelxf


看雪閱讀推薦:

相關文章