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 変数
エラーが起きたときに変数のアドレス (特にシングルトンのアドレス) の出力が役に立つ場合もありますが、アドレスよりさらに便利な変数もいくつか存在します:
mfunc
/jl_uncompress_ast(mfunc->def, mfunc->code)
(jl_apply_generic
内で): コールスタックに関する情報が (多少) 手に入ります。jl_lineno
/jl_filename
: どのファイルの何行目をデバッグすればいいのか (あるいはファイルがどこまでパースされたのか) が分かります。$1
: 最後の GDB コマンド (例えばprint
) の結果を参照できます。厳密には変数ではありません。jl_options
: 正しくパースできたコマンドラインオプションがリストされます。jl_uv_stderr
:stdio
を扱いたくない場合に有用です。
Julia 変数を調べるのに便利な Julia 関数
jl_gdblookup($rip)
: 現在の関数と行番号を確認します (i686 プラットフォームでは$eip
としてください)。jlbacktrace()
: Julia バックトレーススタックをstderr
にダンプします。この関数を使うにはまずrecord_backtrace()
を呼ぶ必要があります。jl_dump_llvm_value(Value*)
: GDB からはValue->dump()
を呼び出してもそのままでは上手く動きません。代わりにこちらを使ってください。例えばf->linfo->functionObject
,f->linfo->specFunctionObject
,to_function(f->linfo)
などを渡します。Type->dump()
: LLDB でだけ利用できます。LLDB がプロンプトで出力を上書きしないようにするには;1
などとします。jl_eval_string("expr")
: 現在の状態を変更する副作用を引き起こす、あるいはシンボルを検索するために使います。jl_typeof(jl_value_t*)
: Julia 値の型タグを取り出します。GDB では呼び出す前にmacro define jl_typeof jl_typeof
が必要です (第一引数をty
として省略形で定義しても構いません)。
GDB からブレークポイントを仕込む
GDB セッションからブレークポイントを jl_breakpoint
に設定するには次のようにします:
(gdb) break jl_breakpoint
こうした上で、Julia コードから次の ccall
で jl_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 のビルドは二つのステージからなり、それぞれ sys0
と sys.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
のメソッドに名前がユニークになるよう接尾辞を付けたものです。このフレームの f
は jl_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 を実行したほうがよいかもしれません。