CPythonのバイトコードをgdb上でステップ実行するための下準備

この投稿では，PythonのC言語実装であるCPythonにおいて，インタプリタ内部でPythonのソースコードがどのように処理されていくかをgdbで追っかけるための下準備について説明します。ソースコードを頭から読んでいくと日が暮れてしまうため，インタプリタとしての中心的な役割を果たす\_PyEval_EvalFrameDefault関数に焦点を当てみたいと思います。実行環境はmacOS Sierra,Pythonは3.6.3,gdbは8.0.1です。

#はじめに
前提として，gdbが起動した状態でPythonの対話モードをプロンプトが表示される状態まで処理を移動させ，そこで入力したコードの挙動を確認していきます。Pythonは処理が実行されるときにバイトコードに変換されます。CPythonでは，バイトコードを構成する各命令コードに処理がそれぞれ割り当てられています。このある命令コードからある処理への割り当てを担当するのが_PyEval_EvalFrameDefault関数です。この関数内に，各命令コードを繰り返し実行するforループが存在し，forループ内のswitch文に各命令コードに対する処理が記されています。

#gdbで_PyEval_EvalFrameDefault関数の先頭に移動する

それでは，Pythonのコードをgdb上で確認できるよう対話モードのプロンプトが表示される箇所まで処理を移動してみます。gdbをシェルから起動した後，\_PyEval_EvalFrameDefault関数に直接ブレークポイントを割り当てr(un)コマンドを実行しても，対話モードのプロンプトは表示されません。これは，対話モードに入る前のインタプリタの初期化処理の段階で\_PyEval_EvalFrameDefault関数が使用されているためです。これを回避するため，初期化処理完了後しか実行されないPyRun_InteractiveOneObject関数にブレークポイントを設置して，まずはそこまで移動した後，\_PyEval_EvalFrameDefault関数にブレークポイントを割り当て，関数の先頭に移動します。（gdbが下記のとおり動作しない場合は，[以前投稿した記事](https://qiita.com/satote2/items/6929083031ccd16ad6aa)参考にしてください。）

```shell-session
$ gdb python.exe
GNU gdb (GDB) 8.0
...
Reading symbols from python.exe...done.
(gdb) b PyRun_InteractiveOneObject  => まずは，PyRun_InteractiveOneObject関数にブレークポイントを設定
Breakpoint 1 at 0x10024f0c5: file Python/pythonrun.c, line 154.
(gdb) r 　　　　　　　　　　　　　　　 => とりあえずr(un)コマンドでプログラムを実行
Thread 3 hit Breakpoint 1, PyRun_InteractiveOneObject (fp=0x7fffc8bc0110, 
    filename='<stdin>', flags=0x7fff5fbff9b8) at Python/pythonrun.c:154
154	    PyObject *m, *d, *v, *w, *oenc = NULL, *mod_name;

(gdb) b _PyEval_EvalFrameDefault  　=> その後に，PyEval_EvalRrameDefault関数にブレークポイントを設定
Breakpoint 2 at 0x1001dfa49: file Python/ceval.c, line 768.

(gdb) c　　　　　　　　　　　　　　　 => c(ontinue)コマンドで処理が停止する。 => プロンプトが表示される
Continuing.
>>> 
```

このように，_PyEval_EvalFrameDefaultとPyRun_InteractiveOneObjectにブレークポイントを設置することによって，プロンプトが表示されるポイントとバイトコードの実行が開始される位置に停止できるので非常に便利です。

#バイトコードへの変換

\_PyEval_EvalFrameDefault関数の中身について説明する前に，追いかけてみたいPythonのコードがどんなバイトコードに変換されるかを確認する方法を紹介します。Pythonを起動した後，下記のようにdisモジュールを使用して，バイトコードを表示させることができます。（下記は"a = 1"をバイトコードに変換した例）

```py
>>> import dis
>>> dis.dis("a = 1")
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (a)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE
>>> 
```
disモジュールは，シェルからも直接実行することもできます。（ファイル"a.py"には，"a = 1"と記述）

```py
$ python3 -m dis a.py
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (a)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE
$
```
#_PyEval_EvalFrameDefault関数内のブレークポイントの設置
ステップ実行の対象となる\_PyEval_EvalFrameDefault関数について説明します。この関数はPython/ceval.cの768行目に存在しており，1143行目にバイトコードを逐次実行するためのforループがあります。そのforループ内にの1266行目にswitch文の先頭があります。ここ処理対象となる命令コード(opcode変数)を判定して，各命令コードにジャンプします。このようにバイトコードの実行は1266行目にあるswitch文が起点になることから，ここにブレークポイントを設置することでバイトコードを順番にステップ実行することができます。

##FAST_DISPATCHマクロ
switch文が起点になると書きましたが異なるケースが存在します。一般的な繰り返し処理では１つの処理が完了した後にfor文の先頭に戻して次の処理を行いますが，CPythonではインタプリタとしての処理速度を向上させるためswitchに移動しないケースがあります。それを実現するのがFAST_DISPATCHマクロです。FAST_DISPATCHを利用することで「for文の先頭に戻りswitch文で次の命令コードを判定する」処理を飛ばして，次の命令コードの先頭にジャンプすることができます。上記の"a = 1"のような例では，LOAD_CONSTの処理の最後でFAST_DISPATCH()マクロを実行しているため，1266行目を経由せず，STORE_NAME(Python/ceval.c:2211)に直接移動します。
＊補足：FAST_DISPATCH()の位置にDISPATCH()がある場合，continue文が実行され，forループの先頭(1143行目)に戻ると想定したのですが，gdbでステップ実行するとFAST_DISPATCH()と同様に1266行目のswitch文を経由せずに次の命令コードに移動します…FAST_DISPATCHとDISPATCHの違いがわからない…。

#まとめ
上記の内容を整理します。バイトコードをgdbでステップ実行するときは以下の手順を踏みます。

1. まずはPyRun_InteractiveOneObject関数に移動後，\_PyEval_EvalFrameDefault関数にブレークポイントを設定する
2. Python/ceval.cの1266行目(switch文)にブレークポイントを設定する

このことをおさえておけば，Pythonのソースコードの動作をバイトコードの処理の手前から追うことが可能になります。これを利用してバイトコードを追いかけた例として[こちらの投稿](https://qiita.com/satote2/items/fd41bc5f5a2e2eb76e2c)を用意しました。


#本編外(メモ置き場)

CPython内部では，命令コードの処理に到達する前にさまざまな前処理が行われています。要点となる箇所をメモします。

[ PyRun_InteractiveLoopFlags関数(Python/pythonrun.c:84) ]
対話モードの本体となる関数。標準入力，対話モードで出力するプロンプト(">>> ")を設定する。111行目にforループがあり，112行目にあるPyRun_InteractiveOneObject関数を繰り返し実行する。	

[ PyRun_InteractiveOneObject関数内部(Python/pythonrun.c:212) ]
ここでPyParser_ASTFromFileObject関数をコール。この関数の中ではPyParser_ParseFileObjectとPyAST_FromNodeObjectを利用して，ファイルに記述されたコード(もしくはプロンプトから取得したコード)を構文木およびAST(抽象構文木)に変換している。
　　
[ PyRun_InteractiveOneObject関数内部(Python/pythonrun.c:227) ]
PyImport_AddModuleObject関数をコールしている。モジュールの実体を返す。（対話モードの場合，引数に\_\_main__がセットされる。）

[ PyRun_InteractiveOneObject関数内部(Python/pythonrun.c:232) ]
PyModule_GetDict関数をコール。モジュールの実体からディクショナリ(グローバル変数を格納)を取得している。

[ PyRun_InteractiveOneObject関数内部(Python/pythonrun.c:233) ]
run_mod関数をコール。内部でPyAst_CompileObject関数およびPyEval_EvalCode関数をコールして，ASTをバイトコードに変換，そのコードを実行。

[ run_mod関数内部(Python/pythonrun.c:277) ]
PyAst_CompileObject関数をコール。ASTをバイトコードに変換。

[ run_mod関数内部(Python/pythonrun.c:980) ]
PyEval_EvalCode関数をコール。バイトコードを実行。PyEval_EvalCode関数はPyEval_EvalCodeEx関数を経由して，\_PyEval_EvalCodeWithName関数を呼び出す。

[ \_PyEval_EvalCodeWithName関数内部(Python/ceval.c:3916) ]
PyFrame_New関数をコール。バイトコードを実行するためのフレームを作成。

[ \_PyEval_EvalCodeWithName関数内部(Python/ceval.c:3925) ]
PyDict_New関数をコール。キーワードパラメータのためのディクショナリを作成。

[ \_PyEval_EvalCodeWithName関数内部(Python/ceval.c:3952) ]
PyTuple_New関数をコール。ポテンシャル引数？のためのタプルを作成。

[ \_PyEval_EvalCodeWithName関数内部(Python/ceval.c:4153) ]
PyEval_EvalFrameEx関数をコール。この関数では，現行のスレッドを取得して，そのスレッドに上記で生成したフレームをセットしてバイトコードを実行する。対話モードの場合は，PyEval_EvalFrameDefault関数が実行される。

[ PyEval_EvalFrameDefault関数(Python/ceval.c:757) ]
上記で説明した関数。