この記事では、筆者が最近Pythonのバイトコードを使用した経験をご紹介したいと思います。正確には、使用したのは専らCPythonインタープリタ向けのバイトコードで、バージョンは2.6と2.7に限定されていました。
Pythonは柔軟性の高い言語で、コマンドラインから実行すると主に次のステップをトリガします。
Pythonのインタープリタはスタックベースであるため、そのデータフローを理解するには各命令(命令コードと引数)がスタックに与える影響を知る必要があります。
バイナリファイルのバイトコードを取得する最も簡単な方法は、次のようにCodeTypeの構造体をアンマーシャルすることです。
import marshal
fd = open('path/to/my.pyc', 'rb')
magic = fd.read(4) # python version specific magic num
date = fd.read(4) # compilation date
code_object = marshal.load(fd)
fd.close()
code_objectには、読み込まれたファイルのモジュール全体を表現するCodeTypeオブジェクトが格納されます。このモジュールのすべてのネストしたコードオブジェクト(クラス宣言やメソッドなど)を検査するには、次のようにCodeTypeのコンスタントプールの検査を反復処理する必要があります。
import types
def inspect_code_object(co_obj, indent=''):
print indent, "%s(lineno:%d)" % (co_obj.co_name, co_obj.co_firstlineno)
for c in co_obj.co_consts:
if isinstance(c, types.CodeType):
inspect_code_object(c, indent + ' ')
inspect_code_object(code_object) # We resume from the previous snippet
この場合、各親の下でネストしているコードオブジェクトのツリーが出力されます。次の単純なコードを実行してみましょう。
class A:
def __init__(self):
pass
def __repr__(self):
return 'A()'
a = A()
print a
次のようなツリーが得られます。
<module>(lineno:2)
A(lineno:2)
__init__(lineno:3)
__repr__(lineno:5)
テストのため、compileディレクティブを使用してPythonのソースコードを含む文字列からコードオブジェクトを取得してみましょう。
co_obj = compile(python_source_code, '<string>', 'exec')
コードオブジェクトの検査の詳細については、Pythonのドキュメントでco_*フィールドを参照してください。
コードオブジェクトを取得したら、実際にその(co_codeフィールド内の)逆アセンブルを見ていきましょう。バイトコードを解析して理解することには次の意図があります。
disモジュールのdisassemble関数を実行すると、その方法が示されます。これにより、前述のコード例からの次の出力が得られます。
2 0 LOAD_CONST 0 ('A')
3 LOAD_CONST 3 (())
6 LOAD_CONST 1 (<code object A at 0x42424242, file "<string>", line 2>)
9 MAKE_FUNCTION 0
12 CALL_FUNCTION 0
15 BUILD_CLASS
16 STORE_NAME 0 (A)
8 19 LOAD_NAME 0 (A)
22 CALL_FUNCTION 0
25 STORE_NAME 1 (a)
9 28 LOAD_NAME 1 (a)
31 PRINT_ITEM
32 PRINT_NEWLINE
33 LOAD_CONST 2 (None)
36 RETURN_VALUE
これで以下の情報が取得されます。
インデックス6を見ると、LOAD_CONST opcodeはco_constsタプルから読み込むオブジェクトを指すopargを取得しています。ここでは型宣言Aを指します。すべてのコードオブジェクトの逆コンパイルを反復処理することにより、モジュールのバイトコード全体を取得できます。
バイトコードの最初の部分(インデックス0~16)はAの型宣言に関連し、残りの部分はAをインスタンス化し、出力するコードを示しています。このコードにも、バイトコードや型の変更などを予定していない場合には適さないコンストラクタがあります。
命令コードは総体的に簡潔ですが、次の処理を経ているために奇妙に見えるものもいくつかあります。
最初のカテゴリでは、ソースが変数のシーケンスを代入するとどうなるかを見ていきます。
(1) a, b = 1, '2'
(2) a, b = 1, e
(3) a, b, c = 1, 2, e
(4) a, b, c, d = 1, 2, 3, e
上記の4つのステートメントは全く異なるバイトコードを生成します。
最初のステートメントは、右辺(RHS)に代入された値が定数のみなので最も単純なケースです。この場合、CPythonはUNPACK_SEQUENCEを用いてタプル(1, '2')を作成し、2つの要素をスタックに設定して、変数aとbのそれぞれについてSTORE_FASTを作成することができます。
0 LOAD_CONST 5 ((1, '2'))
3 UNPACK_SEQUENCE 2
6 STORE_FAST 0 (a)
9 STORE_FAST 1 (b)
ところが、2番めのステートメントでは、右辺に変数があるため、ジェネリック型のケースが呼び出され、そこで式が取り出されます。(ここではLOAD_GLOBALを使用した単純な式)。コンパイラではスタック上の値(インデックス18)から新しいタプルを作成する必要はなく、UNPACK_SEQUENCEを使用します。スタックの一番上の2つの要素を入れ替えるROT_TWOを呼び出すにはこれで十分です(19と22を入れ替えても十分だったかもしれません)。
12 LOAD_CONST 1 (1)
15 LOAD_GLOBAL 0 (e)
18 ROT_TWO
19 STORE_FAST 0 (a)
22 STORE_FAST 1 (b)
3番目のケースは実に不思議です。スタックに式を入れるメカニズムは前のケースと同じですが、この場合は一番上の3つの要素を入れ替えた後、さらに一番上の2つの要素を入れ替えます。
25 LOAD_CONST 1 (1)
28 LOAD_CONST 3 (2)
31 LOAD_GLOBAL 0 (e)
34 ROT_THREE
35 ROT_TWO
36 STORE_FAST 0 (a)
39 STORE_FAST 1 (b)
42 STORE_FAST 2 (c)
4番目のケースはジェネリック型のケースを表し、ROT_*を使った処理はこれ以上できないらしく、タプルを作成し、UNPACK_SEQUENCEの呼び出しでタプルをスタックに追加しています。
45 LOAD_CONST 1 (1)
48 LOAD_CONST 3 (2)
51 LOAD_CONST 4 (3)
54 LOAD_GLOBAL 0 (e)
57 BUILD_TUPLE 4
60 UNPACK_SEQUENCE 4
63 STORE_FAST 0 (a)
66 STORE_FAST 1 (b)
69 STORE_FAST 2 (c)
72 STORE_FAST 3 (d)
最後にご紹介する興味深い例は、callコンストラクタと呼び出しを作成する4種類の命令コードに関するものです。この命令コードの数は、インタープリタのコードを最適化するために想定しました。なぜなら、Javaのようにinvokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtualのいずれか1つがあれば意味を成すというわけにはいかないからです。
Javaのinvokeinterface、invokespecial、invokevirtualは言語の静的型付けから派生しています(またinvokespecialはコンストラクタとスーパークラスAFAIKの呼び出しのみに使用されます)。invokestaticは自己記述型(スタックにレシーバを追加する必要がない型)ですが 、Pythonにはそのような概念がありません(インタープリタで処理し、デコレータを使用しない)。つまり、Pythonで呼び出すとしたら、必ずinvokedynamicで翻訳されることになります。
ここではPythonのさまざまなCALL_*命令コードは取り上げていません。なぜなら型付けや静的メソッドがあったり、コンストラクタ用の特殊なアクセスが必要なためです。これらはすべてPythonのメソッド呼び出しの指定方法を対象にしています。文法は次のとおりです。
Call(expr func, expr* args, keyword* keywords,
expr? starargs, expr? kwargs)
calls構造体のコードは次のように記述できます。
func(arg1, arg2, keyword=SOME_VALUE, *unpack_list, **unpack_dict)
キーワード引数はパラメータを位置だけではなく、名前指定で渡すことができます。*はイテラブルからのすべての要素を引数(タプルではなくインライン)として指定し、**はキーワードと値の辞書を想定します。
次の例ではcall siteコンストラクタの可能なすべての機能を実際に使用しています。
バイトコードは次のようになります。
0 LOAD_NAME 0 (func)
3 LOAD_NAME 1 (arg1)
6 LOAD_NAME 2 (arg2)
9 LOAD_CONST 0 ('keyword')
12 LOAD_NAME 3 (SOME_VALUE)
15 LOAD_NAME 4 (unpack_list)
18 LOAD_NAME 5 (unpack_dict)
21 CALL_FUNCTION_VAR_KW 258
通常CALL_FUNCTIONはopargとして関数の引数の数を受けとりますが、ここではその他の情報もエンコーディングされています。1バイト目(0xffマスク)は引数の数、2バイト目(value >> 8) & 0xff)は渡されるキーワード引数の数を指定します。スタックからポップする要素の数を計算するには、次のようにして値を取得する必要があります。
na = arg & 0xff # num args
nk = (arg >> 8) & 0xff # num keywords
n_to_pop = na + 2 * nk + CALL_EXTRA_ARG_OFFSET[op]
CALL_EXTRA_ARG_OFFSETにはcall命令コード個別のオフセット(CALL_FUNCTION_VAR_KWの場合は2)が格納されます。この例では関数名にアクセスする前にポップする要素の数として6が返されています。
その他のCALL_*キーワードに関しては、コードでリスト渡しの引数と辞書渡しの引数のどちらを使用しているかによって決まります。要は組み合わせの問題です。
コードの実際の動作を理解するには、制御フローグラフ(CFG)を作成すると、どのような場合に命令コードのどの無条件シーケンス(基本ブロック)が実行されるかを追跡することができます。
バイトコードは簡素な言語ですが、信頼性の高いCFGを作成するにはこのブログ投稿では書ききれなかった詳細情報が必要なので、CFGの作成を実装する場合にはequipを参照してください。
次に、制御フローがifステートメントのみに依存するループ/例外のフリーコードを中心に説明します。
ジャンプ先アドレスの受け渡しには、次のような短い命令コード(ループなし/例外)を使用します。
関数のCFGを作成するということは、基本ブロック(例外が発生しない限り無条件に実行される一連の命令コード)を作成し、それを条件分岐を含むグラフ内で結合することを意味します。この例で使用している分岐は、True、False、Unconditionalのみです。
次のコード例を考えてみましょう(実際には決して使わないでください)。
def factorial(n):
if n <= 1:
return 1
elif n == 2:
return 2
return n * factorial(n - 1)
前述したように、factorialメソッドのコードオブジェクトを取得します。
module_co = compile(python_source, '<string>', 'exec')
meth_co = module_co.co_consts[0]
これを逆アセンブルすると次のようになります(筆者の注釈は除外してください)。
3 0 LOAD_FAST 0 (n)
3 LOAD_CONST 1 (1)
6 COMPARE_OP 1 (<=)
9 POP_JUMP_IF_FALSE 16 <<< control flow
4 12 LOAD_CONST 1 (1)
15 RETURN_VALUE <<< control flow
5 >> 16 LOAD_FAST 0 (n)
19 LOAD_CONST 2 (2)
22 COMPARE_OP 2 (==)
25 POP_JUMP_IF_FALSE 32 <<< control flow
6 28 LOAD_CONST 2 (2)
31 RETURN_VALUE <<< control flow
7 >> 32 LOAD_FAST 0 (n)
35 LOAD_GLOBAL 0 (factorial)
38 LOAD_FAST 0 (n)
41 LOAD_CONST 1 (1)
44 BINARY_SUBTRACT
45 CALL_FUNCTION 1
48 BINARY_MULTIPLY
49 RETURN_VALUE <<< control flow
このバイトコードにはCFGの構造を変更する5つの命令があります(制約を追加するか、すぐに終了できるようにしています)。
検索対象は制御フローを変更するこれらの命令だけなので、基本ブロックの抽出は容易になります。この例ではフォールスルーを禁止しているジャンプはなく、JUMP_FORWARDまたはJUMP_ABSOLUTEがその処理を実行します。
そのような構造を抽出するコードの例を次に示します。
import opcode
RETURN_VALUE = 83
JUMP_FORWARD, JUMP_ABSOLUTE = 110, 113
FALSE_BRANCH_JUMPS = (111, 114) # JUMP_IF_FALSE_OR_POP, POP_JUMP_IF_FALSE
def find_blocks(meth_co):
blocks = {}
code = meth_co.co_code
finger_start_block = 0
i, length = 0, len(code)
while i < length:
op = ord(code[i])
i += 1
if op == RETURN_VALUE: # We force finishing the block after the return,
# dead code might still exist after though...
blocks[finger_start_block] = {
'length': i - finger_start_block - 1,
'exit': True
}
finger_start_block = i
elif op >= opcode.HAVE_ARGUMENT:
oparg = ord(code[i]) + (ord(code[i+1]) << 8)
i += 2
if op in opcode.hasjabs: # Absolute jump to oparg
blocks[finger_start_block] = {
'length': i - finger_start_block
}
if op == JUMP_ABSOLUTE: # Only uncond absolute jump
blocks[finger_start_block]['conditions'] = {
'uncond': oparg
}
else:
false_index, true_index = (oparg, i) if op in FALSE_BRANCH_JUMPS else (i, oparg)
blocks[finger_start_block]['conditions'] = {
'true': true_index,
'false': false_index
}
finger_start_block = i
elif op in opcode.hasjrel:
# Essentially do the same...
pass
return blocks
これで次の基本ブロックが得られます。
Block 0: {'length': 12, 'conditions': {'false': 16, 'true': 12}}
Block 12: {'length': 3, 'exit': True}
Block 16: {'length': 12, 'conditions': {'false': 32, 'true': 28}}
Block 28: {'length': 3, 'exit': True}
Block 32: {'length': 17, 'exit': True}
現在のブロックの構造は次のようになります。
Basic blocks
start_block_index :=
length := size of instructions
condition := true | false | uncond -> target_index
exit* := true
制御フローグラフ(ブロックのエントリーと暗黙的returnを差し引いたもの)が得られ、それをたとえばドットに変換して表示することもできます。
def to_dot(blocks):
cache = {}
def get_node_id(idx, buf):
if idx not in cache:
cache[idx] = 'node_%d' % idx
buf.append('%s [label="Block Index %d"];' % (cache[idx], idx))
return cache[idx]
buffer = ['digraph CFG {']
buffer.append('entry [label="CFG Entry"]; ')
buffer.append('exit [label="CFG Implicit Return"]; ')
for block_idx in blocks:
node_id = get_node_id(block_idx, buffer)
if block_idx == 0:
buffer.append('entry -> %s;' % node_id)
if 'conditions' in blocks[block_idx]:
for cond_kind in blocks[block_idx]['conditions']:
target_id = get_node_id(blocks[block_idx]['conditions'][cond_kind], buffer)
buffer.append('%s -> %s [label="%s"];' % (node_id, target_id, cond_kind))
if 'exit' in blocks[block_idx]:
buffer.append('%s -> exit;' % node_id)
buffer.append('}')
return '\n'.join(buffer)
Pythonのバイトコードにアクセスすることはきわめて稀ですが、筆者は過去に何度かその機会がありました。この情報がPythonのリバース・エンジニアリング・プロジェクトに着手しようとしている方のお役に立てば幸いです。
このところ筆者はPythonのコード、特にバイトコードを埋め込むことができるかを調査していました。Pythonにはそれを実現する機能がないからです(しかもソースコードを埋め込むと多くの場合にデコレータなどを使用した非効率な挿入コードが残ります)。そこでequipが誕生しました。