pickle で作成したファイルをデバッグしたい場合に、逆アセンブリを読むこともあると思います。
こちらの記事がとても丁寧に解説されていて、非常にわかりやすく助かりました。
ですが、pickletools
のプロトコル(バージョン)が違うからなのか、こちらに掲載されていないニモニックもあり、あまり日本語でまとまっているサイトも見当たらなかったので、いくつか追加で解説していきたいと思います。今回使用したプロトコルは PROTO 4
です。
STACK_GLOBAL (スタックを参照する GLOBAL)
冒頭の記事で紹介されている GLOBAL
オペコードでは 2つの引数を取ってオブジェクトを作成しますが、 STACK_GLOBAL
はかわりにスタックからオブジェクトを2個ポップして、(プッシュされた順に)module_name
と class_name
とし、"module_name.class_name"
をスタックにプッシュします。
使用例
import pickle
from collections import deque
pickle.dump(deque, open('pickle.pkl', 'wb'))
11: \x8c SHORT_BINUNICODE 'collections'
25: \x8c SHORT_BINUNICODE 'deque'
33: \x93 STACK_GLOBAL
上の例ではスタックから文字列 "deque"
と "collections"
を順にポップし、 "collections.deque"
を探してプッシュします。
デシリアライズ時にモジュールが見つからない場合は ModuleNotFoundError: No module named 'xxxxxxxx'
というようなエラーが出ます。また、クラスが見つからない場合は AttributeError: Can't get attribute 'xxxxxxxx' on <module 'xxxxxxxx' from 'xxxxxxxx.py'>
というようなエラーが出ます。
NEWOBJ (インスタンスの作成)
スタックからオブジェクトを2個ポップして、(プッシュされた順に)クラスと、引数を表すタプルとします。それぞれを cls
, args
として、cls.__new__(cls, *args)
が呼び出されます。これをスタックにプッシュします。これにより cls
のインスタンスが作成されます。
import pickle
class Person:
pass
pickle.dump(Person(), open('pickle.pkl', 'wb'))
11: \x8c SHORT_BINUNICODE '__main__'
22: \x8c SHORT_BINUNICODE 'Person'
31: \x93 STACK_GLOBAL
33: ) EMPTY_TUPLE
34: \x81 NEWOBJ
この例では、NEWOBJ
の箇所で Person.__new__(Person)
が呼び出され、従って親クラスの object.__new__(Person)
が呼び出されて、Person クラスの新しいインスタンスが作成されます。しかし、この時点では空のインスタンスが作成されただけであり、Person.__init__
が呼び出されるような副作用はありません。従って、__init__
でインスタンス変数を作成するように実装していたとしても、この時点ではインスタンス変数は作成されません。
インスタンス変数を作成する場合は、元の記事にあるように BUILD
などが使用されます。
MEMOIZE, PUT, BINPUT, LONG_BINPUT (スタックトップに番号を付けて記憶する)
スタックトップに格納されているオブジェクトを記憶し、番号を付けます。この際、スタックは変更されません。BINGET
などと組み合わせて使用されるようです。
BINGET, LONG_BINGET (メモを呼び出す)
番号を指定してメモから値を取り出し、スタックにプッシュします。BINGET 1
は MEMOIZE (as 1)
が実行された時点でスタックトップにある値 (変数 a
) を参照します。BINGET 3
は MEMOIZE (as 3)
が実行された時点でスタックトップにある値 (変数 b
) を参照します。
import pickle
a = { 'B': None }
b = { 'A': a }
a['B'] = b
pickle.dump([a, b], open('pickle.pkl', 'wb'))
11: ] EMPTY_LIST
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: } EMPTY_DICT
15: \x94 MEMOIZE (as 1)
16: \x8c SHORT_BINUNICODE 'B'
19: \x94 MEMOIZE (as 2)
20: } EMPTY_DICT
21: \x94 MEMOIZE (as 3)
22: \x8c SHORT_BINUNICODE 'A'
25: \x94 MEMOIZE (as 4)
26: h BINGET 1
28: s SETITEM
29: s SETITEM
30: h BINGET 3
32: e APPENDS (MARK at 13)
APPENDS (マークまで LIST に追加する)
マークオブジェクトが来るまでスタックの値をポップして、その時点でスタックトップにある list に対して、(プッシュされた順に)値を追加します。
SETITEMS (マークまで SETITEM を繰り返す)
スタックトップからマークオブジェクトまでの偶数個の値をポップし、2個のペアごとに、(プッシュされた順に)それぞれキー、値とします。この時点でスタックトップにある dict に対して、キーと値のペアを追加します。冒頭で紹介した記事で説明されている DICT
オペコードとは異なり、新しく dict を作る代わりに既存の dict に対して要素を追加します。
import pickle
a = { 'x': 1, 'y': 2, 'z': 3 }
pickle.dump(a, open('pickle.pkl', 'wb'))
11: } EMPTY_DICT
13: ( MARK
14: \x8c SHORT_BINUNICODE 'x'
18: K BININT1 1
20: \x8c SHORT_BINUNICODE 'y'
24: K BININT1 2
26: \x8c SHORT_BINUNICODE 'z'
30: K BININT1 3
32: u SETITEMS (MARK at 13)
BININT, BININT1, BININT2, LONG, LONG1, LONG4
スタックに整数をプッシュします。
NONE, NEWTRUE, NEWFALSE
スタックにそれぞれ None
, True
, False
をプッシュします。
UNICODE, SHORT_BINUNICODE, BINUNICODE, BINUNICODE8 (文字列)
文字列をスタックに追加します。
import pickle
a = ['Hello World!']
pickle.dump(a, open('pickle.pkl', 'wb'))
11: ] EMPTY_LIST
13: \x8c SHORT_BINUNICODE 'Hello World!'
28: a APPEND
EMPTY_LIST, EMPTY_TUPLE, EMPTY_DICT
それぞれ空のリスト、タプル、dict をスタックトップにプッシュします。