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

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

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

https://qiita.com/tanuk1647/items/4c2a305c7cc4e12ef99d

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

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

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

使用例

```py: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'>` というようなエラーが出ます。

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1941

# NEWOBJ (インスタンスの作成)

スタックからオブジェクトを2個ポップして、（プッシュされた順に）クラスと、引数を表すタプルとします。それぞれを `cls`, `args` として、`cls.__new__(cls, *args)` が呼び出されます。これをスタックにプッシュします。これにより `cls` のインスタンスが作成されます。

```py: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` などと組み合わせて使用されるようです。

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1864

# BINGET, LONG_BINGET (メモを呼び出す)

番号を指定してメモから値を取り出し、スタックにプッシュします。`BINGET 1` は `MEMOIZE (as 1)` が実行された時点でスタックトップにある値 (変数 `a`) を参照します。`BINGET 3` は `MEMOIZE (as 3)` が実行された時点でスタックトップにある値 (変数 `b`) を参照します。

```py: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)
```

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1803

# APPENDS (マークまで LIST に追加する)

マークオブジェクトが来るまでスタックの値をポップして、その時点でスタックトップにある list に対して、（プッシュされた順に）値を追加します。

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1532

# SETITEMS (マークまで SETITEM を繰り返す)

スタックトップからマークオブジェクトまでの偶数個の値をポップし、2個のペアごとに、（プッシュされた順に）それぞれキー、値とします。この時点でスタックトップにある dict に対して、キーと値のペアを追加します。冒頭で紹介した記事で説明されている `DICT` オペコードとは異なり、新しく dict を作る代わりに既存の dict に対して要素を追加します。

```py: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)
```

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1674

# BININT, BININT1, BININT2, LONG, LONG1, LONG4

スタックに整数をプッシュします。

# NONE, NEWTRUE, NEWFALSE

スタックにそれぞれ `None`, `True`, `False` をプッシュします。

# UNICODE, SHORT_BINUNICODE, BINUNICODE, BINUNICODE8 (文字列)

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

```py: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
```

https://github.com/python/cpython/blob/main/Lib/pickletools.py#L1431

# EMPTY_LIST, EMPTY_TUPLE, EMPTY_DICT

それぞれ空のリスト、タプル、dict をスタックトップにプッシュします。