0
0

pickle の逆アセンブリの命令の解説 (pickletools 使用)

Last updated at Posted at 2023-11-27

pickle で作成したファイルをデバッグしたい場合に、逆アセンブリを読むこともあると思います。

こちらの記事がとても丁寧に解説されていて、非常にわかりやすく助かりました。

ですが、pickletools のプロトコル(バージョン)が違うからなのか、こちらに掲載されていないニモニックもあり、あまり日本語でまとまっているサイトも見当たらなかったので、いくつか追加で解説していきたいと思います。今回使用したプロトコルは PROTO 4 です。

STACK_GLOBAL (スタックを参照する GLOBAL)

冒頭の記事で紹介されている GLOBAL オペコードでは 2つの引数を取ってオブジェクトを作成しますが、 STACK_GLOBAL はかわりにスタックからオブジェクトを2個ポップして、(プッシュされた順に)module_nameclass_name とし、"module_name.class_name"をスタックにプッシュします。

使用例

python
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 のインスタンスが作成されます。

python
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 1MEMOIZE (as 1) が実行された時点でスタックトップにある値 (変数 a) を参照します。BINGET 3MEMOIZE (as 3) が実行された時点でスタックトップにある値 (変数 b) を参照します。

python
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 に対して要素を追加します。

python
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 (文字列)

文字列をスタックに追加します。

python
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 をスタックトップにプッシュします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0