GDB デバッグ Tips

Julia 変数の表示

GDB を実行中に jl_value_t* 型のオブジェクト obj を表示するには次のようにします:

(gdb) call jl_(obj)

オブジェクトは GDB セッションではなく Julia セッションで表示されます。これはオブジェクトの型と値が Julia の C コードによって操作されていることが分かったときに有用です。

同様に、Julia 内部の Julia コード (例えば compiler.jl) をデバッグしているときは、次のコマンドで obj を表示できます:

ccall(:jl_, Cvoid, (Any,), obj)

こうすると Julia の出力ストリームが初期化される順番によって引き起こされる問題を回避できます。

GDB で call fl_print(fl_ctx, ios_stdout, obj) とすると Julia の flisp インタープリタが使用する value_t オブジェクトを表示できます。

内部状態を調べるのに便利な Julia 変数

エラーが起きたときに変数のアドレス (特にシングルトンのアドレス) の出力が役に立つ場合もありますが、アドレスよりさらに便利な変数もいくつか存在します:

Julia 変数を調べるのに便利な Julia 関数

GDB からブレークポイントを仕込む

GDB セッションからブレークポイントを jl_breakpoint に設定するには次のようにします:

(gdb) break jl_breakpoint

こうした上で、Julia コードから次の ccalljl_breakpoint を呼んでください:

ccall(:jl_breakpoint, Cvoid, (Any,), obj)

obj に渡した変数あるいはタプルがブレークポイントからアクセスできるようになります。

jl_apply のフレームまで戻ると関数の引数が確認できるので便利です。次のようにします:

(gdb) call jl_(args[0])

もう一つの便利なフレームが to_function(jl_method_instance_t *li, bool cstyle) です。jl_method_instance_t* 型の引数 li にはコンパイラへ最後に送られた AST の参照が含まれます。ただし通常この時点での AST は圧縮されているので、AST を見るときは jl_uncompress_ast を呼び、その結果を jl_ に渡してください:

#2  0x00007ffff7928bf7 in to_function (li=0x2812060, cstyle=false) at codegen.cpp:584
584          abort();
(gdb) p jl_(jl_uncompress_ast(li, li->ast))

特定の条件で発火するブレークポイントを設置する

特定のファイルをロードしたとき

対象のファイルが sysimg.jl なら、次のようにします:

(gdb) break jl_load if strcmp(fname, "sysimg.jl")==0

特定のメソッドを呼んだとき

(gdb) break jl_apply_generic if strcmp((char*)(jl_symbol_name)(jl_gf_mtable(F)->name), "method_to_break")==0

全ての関数呼び出しでこの関数が使われるので、このブレークポイントを設置すると実行が 1000 倍程度遅くなります。

シグナルの取り扱い

Julia は通常の動作でいくつかのシグナルを利用します。プロファイラは SIGUSR2 でサンプルを行い、ガベージコレクタは SIGSEGV でスレッドの同期を行います。プロファイラや複数のスレッドが関係するコードをデバッグすると通常の実行でシグナルが頻繁に発生するので、対応するシグナルを無視するように設定した方がよいでしょう。GDB では次のコマンドで行えます (SIGSEGV の部分を SIGUSRS などで置き換えてください):

(gdb) handle SIGSEGV noprint nostop pass

LLDB では次のコマンドです (プロセスを開始してから実行します):

(lldb) pro hand -p true -s false -n false SIGSEGV

スレッドを使うコードで起きた segfault をデバッグするときは jl_critical_error にブレークポイントを設置してください。こうすると GC の同期が行われる地点ではなく実際の segfault が起きた地点を捕捉できます (Linux と BSD では sigdie_handler も利用できます)。

Julia のビルドプロセス (ブートストラップ) のデバッグ

make 中に起こるエラーには特別な処理が必要です。Julia のビルドは二つのステージからなり、それぞれ sys0sys.ji が構築されます。エラーが起きた瞬間に実行しているコマンドを確認するには make VERBOSE=1 としてください。

執筆時点において、base ディレクトリから sys0 フェーズのビルドエラーをデバッグするには次のコマンドを使います:

julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys0 sysimg.jl

usr/lib/julia/ にあるファイルを全て削除しておく必要がある可能性があります。

sys.ji フェーズのデバッグは次のコマンドで行えます:

julia/base$ gdb --args ../usr/bin/julia-debug -C native --build ../usr/lib/julia/sys -J ../usr/lib/julia/sys0.ji sysimg.jl

デフォルトの設定だと、GDB 内で実行していたとしてもエラーが起こった時点で Julia は終了します。エラーを “現行犯” で捕まえるには、jl_error にブレークポイントを設置してください。jl_too_few_args, jl_too_many_args, jl_throw など、特定の種類のエラーを捕まえられる関数もあります。

エラーを捕まえたら、スタックの上方を見て jl_apply の呼び出しを探すとよいでしょう。具体的な実行例を示します:

Breakpoint 1, jl_throw (e=0x7ffdf42de400) at task.c:802
802 {
(gdb) p jl_(e)
ErrorException("auto_unbox: unable to determine argument type")
$2 = void
(gdb) bt 10
#0  jl_throw (e=0x7ffdf42de400) at task.c:802
#1  0x00007ffff65412fe in jl_error (str=0x7ffde56be000 <_j_str267> "auto_unbox:
   unable to determine argument type")
   at builtins.c:39
#2  0x00007ffde56bd01a in julia_convert_16886 ()
#3  0x00007ffff6541154 in jl_apply (f=0x7ffdf367f630, args=0x7fffffffc2b0, nargs=2) at julia.h:1281
...

一番近い jl_apply フレーム #3 まで戻って AST を見れば、関数が julia_convert_16886 であることが分かります。これは convert のメソッドに名前がユニークになるよう接尾辞を付けたものです。このフレームの fjl_function_t* なので、specTypes フィールドを見ればシグネチャを取得できます:

(gdb) f 3
#3  0x00007ffff6541154 in jl_apply (f=0x7ffdf367f630, args=0x7fffffffc2b0, nargs=2) at julia.h:1281
1281            return f->fptr((jl_value_t*)f, args, nargs);
(gdb) p f->linfo->specTypes
$4 = (jl_tupletype_t *) 0x7ffdf39b1030
(gdb) p jl_( f->linfo->specTypes )
Tuple{Type{Float32}, Float64}           # <-- julia_convert_16886 の型シグネチャ

この関数の AST も出力できます:

(gdb) p jl_( jl_uncompress_ast(f->linfo, f->linfo->ast) )
Expr(:lambda, Array{Any, 1}[:#s29, :x], Array{Any, 1}[Array{Any, 1}[], Array{Any, 1}[Array{Any, 1}[:#s29, :Any, 0], Array{Any, 1}[:x, :Any, 0]], Array{Any, 1}[], 0], Expr(:body,
Expr(:line, 90, :float.jl)::Any,
Expr(:return, Expr(:call, :box, :Float32, Expr(:call, :fptrunc, :Float32, :x)::Any)::Any)::Any)::Any)::Any

最後に紹介するおそらく最も有用なテクニックは、関数を強制的に再コンパイルさせてコード生成処理をステップ実行するというものです。これを行うには jl_lamdbda_info_t* に含まれるキャッシュされた functionObject をクリアします:

(gdb) p f->linfo->functionObject
$8 = (void *) 0x1289d070
(gdb) set f->linfo->functionObject = NULL

それから適当な場所 (emit_function, emit_expr, emit_call など) にブレークポイントを設置し、コード生成をもう一度起動します:

(gdb) p jl_compile(f)
... # ブレークポイント

事前コンパイルのエラーのデバッグ

モジュールを事前コンパイルすると、モジュールごとに個別の Julia プロセスが起動されます。そのため事前コンパイルを行うプロセスに対してブレークポイントの設定やエラーの捕捉を行うには、デバッガをワーカーにアタッチする必要があります。指定された名前に合致する新しいプロセスの起動をデバッガに監視させるのが最も簡単です。例を示します:

(gdb) attach -w -n julia-debug

LLDB では次のコマンドです:

(lldb) process attach -w -n julia-debug

それからスクリプト/コマンドを実行して事前コンパイルを開始してください。親プロセスで前述した条件付きブレークポイントを使って対象のファイルを読み込むイベントを捕捉すると、実行を追うのが楽になります (一部のオペレーティングシステムでは、親プロセスの fork を追うといった別のアプローチが必要になります)。

Mozilla の記録/再生フレームワーク (rr)

現在の Julia では何もしなくても rr を使用できます。rr は決定的なデバッグのために実行の記録/再生を行う軽量フレームワークであり、Mozilla によって提供されます。rr を使うと実行のトレースを決定的に再現でき、再現された実行のアドレス空間・レジスタの内容・システムコールのデータは何度実行しても正確に元の実行と一致します。

rr の新しいバージョン (3.1.0 以降) が必要です。

rr を使って並行実行のバグを再現する

デフォルトで rr は単一スレッドのマシンをシミュレートします。並行なコードをデバッグするときは rr record --chaos を使ってください。こうすると 1 から 8 までのランダムに選んだ個数のコアを使ってシミュレートが行われます。このためバグを捕まえるまでは JULIA_NUM_THREADS=8 をした上で rr から Julia を実行したほうがよいかもしれません。