radare2のなかみ(radare2 internal)
r2でもTimeless Debugingしたい。(GSoC2017) mid-termまでにrrやqiraのログを読んでTDできるように。finalでデバッガのプラグインとしてレコーダも開発。
- dsb(debugger step back)を作れとの事だが、これは1ステップバックコマンド。で、ステップ"オーバー"するべきなの? 関数呼び出した後、後ろに戻ったら関数内のretに戻るべき?
- qiraとかrrのtracing sessionはどうやって読み込む? r2実行時コマンドオプションで渡す? 読み込むタイミングは?
- dsbの実装案、RDebugがその時のデバッギの状態っぽい。これを前の状態に戻す。dsoの場合r_debug_step_overからptrace(SINGLE_STEP)でデバッギの状態を更新 じゃあdsbやろうと思うと、PC含めレジスタの書き換えができるのでptraceで制御を1命令戻すとかは可能。なので必要なのは、デバッグログ。このログを読んでいってptrace で状態を前に戻していけば良さそう。
- ログに沿ってメモリ、レジスタの値を前に戻す、どうやる? 例えばqiraのログは命令ごとにどこに何を書きこんだかとかは分かるが、書き込み前のデータはわからないので 復元出来ないのでは。qiraのメモリダンプとかは、ちょうどカーソルが今あるclnum行のメモリアクセスログだけ取ってきて、ダンプに反映させるような仕組み。割と適当。 つまり、 mov [0x100], $before mov $hoge, $hoge2 mov [0x100], $after この時clnum=3->2にdsbしたら問題発生。 [0x100]の値がclnum=2では不定になる。 つまりqiraはコミットログをdiffで取っておらず、変化する前の値というものをログに持っていないため、clnum=3での [0x100]への$afterの代入を取り消したら、[0x100]というファイルがどう元に戻るのかが分からない。 一般的なRnRツールは、こういう場合3->2に戻る操作をしたら、裏で先頭から2までの動作をもう一度順向きに流して復元するのが一般的らしい。副作用的にも(大居さんいわく)
- 今の所良さそうな実装としてはRSnapshotでプログラムの最初の状態をとっといて、reverse-系のコマンド実行したらそこに復元。そこからqiraのログに沿って実行していく。ただし最初の状態を取っておく必要があって、自動的にdmsとかするとデフォルトで何かsnapshotがある状態。(ちょっと気持ち悪い) じゃあgdbっぽく、ユーザーが dmsaとかで記録を開始してスナップショットを取っておいて、その後dsbとか実行して戻るようにする。これで常にdmsa <--> dsbの間なら戻れるようになる。 ただ一つ厄介なのが、これはdmsaを取った位置 < dsb実行した位置 が成立すれば問題ないが、これが壊れる場合がある。なぜなら一度dmsaで取ったsnapshotは、ooとかで 2度目のデバッグを再開始しても引き継がれるので、この不等式が成り立たない区間が存在し、そこでdsbを実行すると正常なリバース実行にならない。 なのでやはりsnapshotには絶対的な指標、それを取った命令位置(offset?)をキーとして持たせておいたほうが良い。
diff形式のメモリスナップショット 毎回全メモリダンプをスナップショットするのはメモリの無駄なので、命令実行で変化した領域だけ差分として保存しておきたい。 フォーマットとしてはqcowみたく、最初にベースRDebugSnapがあって、あとはRDebugSnap->historyとかのリストが差分を古い順に保存してる。 スナップショット保存時は、まず指定されたアドレス(例えば特定のマップ領域)から既にそこのメモリスナップショットがあるか検索。無かったらベースファイルとして そのエリア全体をダンプしてRDebug->snapshotsにつなぐ。もし既にsnapshots内にあるなら、該当するsnapshotのhistoryに、ベースとの差分を追加するだけにする。 でそのhistoryにつないだやつをRDebugSession->memlistが指すようにする。 差分のとり方は内容を適当に1ページごとに見ていって(1バイトごとに見るのは遅すぎる)ハッシュ(SHA256)を比較することで その1ページ内に差分があるかを確認する。あったら新しい1ページを差分として保存。(ここで1ページまるごと差分とするか、詳細にみて本当の差分を取るか....?) このCRCの計算はスナップショットを新規に丸ごと作成する時に計算しておく。また新しい差分データを取った時もその1ページの差分データのCRCも計算しておく。 次回の差分比較時は、この新しい方の1ページの差分を見る。差分はより新しいページのデータと取るべきなので。 メモリスナップショットのIDはbaseを0として、続いてdiffを順番に1,2,3,4...と続く。 例えば、dmsA 0とすればbaseの状態にまで戻る。2なら2まで戻す。ここで注意が必要なのが、2まで戻すときに現在のメモリ領域は3,4等の 他のdiffで変更されてる可能性があるのでこちらも戻す必要がある。つまりアルゴリズムは、全部のdiffを見ていって2より前の該当ページ はdiffを適用して、2より後ろのdiffはdiff自体じゃなくて、baseの該当ページの内容を適用する。これでbaseまで内容が戻る。
セッションキー sessionは現状RDebugKeyを持っていてこいつはsessionを取った場所、アドレスを保持している。 案としては、普通にsessionを取った順番にIDが連番で付いているので、sessionリストの末尾が一つ前の状態になっているはず。こいつを適用させる。ただしdsb等で前に戻って行くと、いくつかのsessionを飛び越して前に戻る。そうするとsessionリストの末尾が一つ前の状態になるとは限らないので、飛び越したタイミングでリストの末尾をそこまで戻す。
ページマップ 2つのsessionでそれぞれページマップが違うとき、例えば途中でmmapが発行されてマップが増えたり、mprotectで権限が変わったりなどは前のsessionのページマップの状態に戻さないとうまく動かない。例えば典型例だとプログラムの最初_startではglibcはマップされてなくて、mainまで進んでマップされる。この時_startでsessionを取っていてそこから戻るとなると、そこまでメモリマップを戻す、すなわちglibcをアンマップしないといけない。
セッションの調整 セッションのポインタはアドレス<-->{id, session}という感じでsdbで管理しておく。 dts+でセッションを追加した際は、sdbに登録しておく。 dcbの事を考えるとややこしい。dcbの場合はチェックポイントとか全く考えず愚直に毎回baseから更新する。
td:al; 非決定的な入力だけログファイル(TraceFrame)にRecordしておく。
recordに関してはRecordCommand.ccのrecordからRecordSession.ccのrecord_stepを回し続ける。record_stepでは状態に変化があったプロセスをスケジューラから取り出して記録。これはsignalだったりptrace eventなどで変化する。 ログデータであるところのTraceFrameはTraceWriter(TraceStream.cc)->write_frameで記録する。これはRecordTaskの各イベントの種類ごとにrecord_**が呼ばれて記録される。このrecord_**は種類によってログデータdata(RAW_DATA),events()等へ書き込む。 それからレジスタやメモリの状態を持ってるcheckpointとかmarkとかの記録はRecord時には行わない。Replay時にエミュレートして記録する。
Replayに関してはReplayCommand.ccのreplayが行う。replayは2プロセスで行う。子プロセスが独自のGDBServerを動作させ、親プロセス側でgdbを起動してそこからデバッグコマンドとか送信。まずはreplay(trace_dir,flags)のtrace_dirにrecordしたトレースディレクトリを与えて、こいつからReplaySessionを初期化(::create)する。ここからadvance_to_next_trace_frameでトレースファイルをtrace_frameに読む。 replay中のデバッグコマンドはGdbServer.serve_replayが受け取って、処理する。serve_replayからdebug_one_stepのループでコマンド取得、実行。 多分ちゃんとしたコマンド実行はここでやってる。つまり1ステップリバースとかもこのdebug_one_stepの後でReplayTimeline::reverse_singlestepとか呼び出して処理。こいつは最初にseek_to_before_keyで現在のキーより前のキーをmarks_with_checkpointsというチェックポイントリストから探してcurrent_keyにコピー。これを繰り返して、戻れる所まで遡っていく。 このmarks_with_checkpointsはadd_explicit_checkpointで追加される。これはset_short_checkpointとかmaybe_add_reverse_executionとかから呼ばれる。 この前のチェックポイントcurrent_keyというのは現在のkeyより記録されたtimeやtickが前のもの。この前のチェックポイントにseek_to_before_keyで戻すが、あまりにも遠かった場合は、maybe_add_reverse_executionで近くにチェックポイントを置き直す。このチェックポイントまでreplay_step(constraints)でconstraintsを満たすまで順番にReplayを回す。 reverse_continueは replay_step内では非決定的なイベントをエミュレーションするtry_one_trace_stepを実行する。ここで、例えばemulate_async_signalとかで 非同期シグナルをログを元に復元する。continue_or_stepで途中からはプログラムの実行をptraceでネイティブにやる。 continue_or_stepは中からresume_executionを呼んでptraceで実行。 こんな感じで、Replayerは
そこからprocess_debugger_requestでinterupt/restart/detachとかのメッセージを処理(?)、それ以外はdispatch_debugger_request。 このprocess_debugger_request内ではいかなるメッセージでも必ず最初にtry_lazy_reverse_singlestepsを実行している。 次にreverse-nextなどの遡り機能の実装。これはtry_lazy_reverse_singlestepsでReplayTimelineのDBから記録を読みだして遡る。このtry_lazy_reverse_singlestepsはdebug_one_step->process_debugger_requestからreverse-系のコマンドが来たら実行される。 debug_one_step内でcompute_run_command_for_reverse_execが実行されてreverseのために こいつはReplayTimeline.ccのlazy_reverse_singlestep(from)でfrom以前のログ記録の位置Markを取得する。実際に取得してるのは、find_singlestep_beforeでmarksから探す。MarkはInternalMarkへのポインタを持ってて、これのラッパー。見つけたらdispatch_regs_requestでregs()とかをgdbクライアントに送信。seek_to_markで現在の位置を移動させて完了。 これらのMarkはReplayTimeline::mark()で現在の位置で追加される。これらmarkはmarksで管理されてる。marks[key][i]でkeyまでの箇所で付けられたi番目のmarkが取得できる。mark()ではcurrentのkeyからmarks[key]を取得して、ここのどこに現在のmarkを追加するか決める。入れる位置はmark_indexでそこにnew_marksをinsertする。現在地のセッションをコピー(clone)してtmp_sessionを作って、ステップ実行していく。でequal_stateでtmp_sessionと同じセッションが出てきたらその直後に追加する。
要するにReplayTimelineは
メモリマップの復元 以前の状態にrestoreするのはReplaySession::cloneを使っている。この中でcopy_state_to()で新しく作ったsessionに状態を戻す。ReplaySessionはSessoinを継承していて、このSession(Session.h)がmap<uid, AcddressSpace*> AddressSpaceMapで表されるプロセス(正確にはタスクグループリーダー)のUIDからメモリマップの対応を持っている。 AddressSpaceMap vm_mapがそれ。 AddressSpaceはタスクグループ(Linuxでいうプロセスグループ?)のメモリマップを管理している。要するに親子プロセスとかも典型的なプロセスグループで、そのへんで主に使うっぽい。またstd::map<MemoryRange, Mapping, MappingComparator> MemoryMapというのもあって、MemoryRangeはアドレスの開始、終端を持ってる範囲を表すクラス。 Mappingはメモリマップの情報を持つ。 さてこれらのメモリマップのデータはレコード時にシステムコールをエミュレートする際に逐一記録しておく。AddressSpace::mapとかbrkとかがそれ。record_syscall.ccのrec_process_syscall_archがエミュレートしてる。これはptrace
#radare2 radare2自体はlibrにあるフロントエンド。今回はr2 -d ./testとかで起動するデバッガを見る。(binr/radare2/radare2.c) pfileにはバックエンドを示す文字列"dbg://"が入る。デフォルト起動だとdbg://でgdb,qemu://とかにもできるみたい。 デバッグ対象のファイル名指定だと基本的に925行目あたりから始まる。debugbackendがデフォルトだと"native"。 最初にr_core_file_openでdbg.profileで設定されたファイルを読んでいる。これは-RオプションでRarun2によって実行される自動実行スクリプト的な(.rr) 次にr_debug_use(libr/debug/plugin.c)で使用するデバッガ(r->dbg)を設定。デフォルトだと"native"。 ここではr->dbgに設定されているプラグインの中から名前に一致する物、例えば"native"をr->dbg->hに設定。実際(libr/debug/p/)にdebug_native.soがあって これを読み込んでいる。以後デバッガはこのプラグインで動作する。 getBaddrFromDebuggerでr->dbgを使ってデバッギにアッタチ(r_debug_attach)してベースアドレスを取得する。デバッギの絶対パス名をマップアドレスから検索してるだけっぽい。 r_core_bin_loadでr->binにファイルをロード? r_core_setup_debuggerはデバッガの初期設定を設定してるっぽい。後はコンフィグファイルのfile.analyzeとか見て、必要であれば初期に"aa"とか実行して解析しとく。 次にrun_commandsでradare2rcに記述されたコマンドスクリプトと-cで指定されたコマンドを一緒に実行する。 そしてr_core_prompt_loop(libr/core/core.c)がユーザーがコマンドを打ち込んで実行するメインループを担当。 r_core_prompt_execで1つずつコマンドを実行。 r_core_cmdでコマンドのバリデーションとか改行で区切るとかして、r_core_cmd_substから[repeat]とか処理しつつ r_core_cmd_subst_iで[cmd]以下全部パースしてオプションとか設定してr_cmd_call(cmd_api.c)でコマンド実行。 このコマンドはr_cmd_addで追加できる。デフォルトだとlibr/core/cmd.cのr_core_cmd_initでコマンドが追加されている。 ここではデバッガ起動時の"debug"コマンドを見てみる。 r_cmd_add (core->rcmd, "debug","debugger operations", &cmd_debug); となっていてcmd_debug(libr/cmd/cmd_debug.c)がデバッガのコマンド群。 ステップオーバーdsoコマンドを見てみる。まずr_reg_arena_swapでarenaにregsetをswaptした後代入(?) このarenaはレジスタの値が格納されてる抽象化エリア。(後述) 次にr_debug_step_over(debug.c)でステップ実行を行う。 この関数ではまずdbg->h->step_overがあればそれを使う。なければr_debug_reg_getでPCを取得し、iob.read_atで命令をbufに読み込み、 r_anal_opでbuf解析してopにオペコード代入。でオペコードが関数呼び出しとかだとステップ"オーバー"する必要があってr_debug_continue_until、 そうでないならr_debug_stepで1ステップだけ進める。前者はr_debug_continue_killとかでdbg->h->contを呼び出し、これはdebug/p/debug_native.c のr_debug_native_contでptrace(PT_CONT)とかやる。後者のステップ実行はr_debug_step_(soft|hard)で別れる。softだとインタプリタ的にやって hardだとdebug_native.cのr_debug_native_stepでptrace(PT_STEP)をやる。
#Register radare2でのレジスタの扱いはアーキテクチャ非依存になるように設計されてる。libr/include/r_reg.hのタイプ(RRegisterType)とID(RRegisterID) の組み合わせで基本的にアクセスして、実際のアクセスされたレジスタが何になるかはreg_arenaというエリアに入っている情報で決まる。 radare2側からはr_reg_getとかでレジスタの値を取ったり、レジスタの名前を取ったりする。 データ構造としてはRReg->name[name]でタイプにあったレジスタの名前、 RReg->regset[type]でレジスタのタイプにあったアーキテクチャごとのレジスタがリストRRegSet(双方向リストRList)が入っている。あとpush/popとかで RRegごとに持ってるpoolにその時のレジスタの値を積んでおいたり下ろしてきて現在の値にするとかもできる。あとarena.cにarenaのレジスタ値をまるごとバイナリ塊 にして返してきたりする関数とかあるけど、逆向きが無くて役に立たない。 ちなみにこのarenaのデータはr_debug_reg_sync(debug/dreg.c)を必要になったら呼び出すことで同期を取る。例えばdsoコマンド実行すると ptraceで速攻デバッギに反映されるが、radare2上のarenaは更新されていないっぽい。でdrみたいなレジスタ表示とかで必要になったら、同期してデバッギから レジスタの値取ってくる。radare2<-->デバッギの間のキャッシュみたいな役割。 こいつはptraceを使ってデバッギの実際のレジスタに書きこんだり読みこんだり同期する。 この読み書きにはdebug_native.cとかのread/write_regが使われる。
それからdr系の命令は結構あって、前回実行時のレジスタ値とかも表示できる。差分も勿論。プロファイルみたいなものを持っている。 例えばdrdコマンドは差分だけ表示。これらはr_debug_reg_listで差分を計算する。 ここではまずr_reg_get_value(dbg->reg)で現在のレジスタの値を取得して、r_reg_arena_swapで以前のレジスタ値に設定。 差分を計算して再度r_reg_arena_swapで元に戻す。でcb_printfとかで表示。
r2にはRDebugSnapというデバッグスナップショットがある。dms*系のコマンドで、アドレス指定するとその時のアドレス範囲のメモリダンプを スナップショットとして保存しておく機能。現状は変更点のみとかじゃなくて、アドレス範囲全部保存したり、リストアしたり。 あとdmsdでdiffが取れるけど、これはスナップショットの全データを見て比較する。割と適当な実装。
dt系のコマンドでコールトレースを作れる。1命令ずつステップ実行していってコール命令だったらadd_trace_tree_childでノード追加。 あとRDebugTraceで何か取るコマンドがあるが未実装。 基本的にはat系のコマンドを使うっぽい。
dbg->h->frame()を呼ぶと、関数の静的解析(?)的な何かをやって、現在のアドレス情報からどういう関数呼び出しでここまで来たかを解析してRDebugFrameのリストを 返してくる。
命令の実行された順番的に前を探すとか、どうやる? 実行順番はそもそもプログラムを実際に動かしてみないと分からない。アドレスとかだと判定しかねる。 ただしタイムスタンプはダメ。これは2度目のデバッグに入ったらアウト。一番愚直なのは、グローバルにカウンタを持っておいて、スナップショット取ったらそのカウンタをID にする。 ブレークポイントを前の命令に打つとか出来ない。できるとすれば今の命令にブレークを打って実行だが、それだと止まった時点でPCが今の命令を指してる。 なので、ステップオーバーで順番に実行していって、anal_opで次の命令を見て
ブレークポイントはRBreakpointという構造体で管理されている。最初にdebug.cでdbg->bpに初期化されてる。 ブレークポイントの追加は同じくdebug.cのr_debug_bp_addで指定されたアドレスか領域(module)に追加される。 でr_bp_add_(sw|hw)のどちらかでブレークポイントを実際にうつ。これらはr_bp_addのラッパー これはRBreakpointItemを新たに作ってRBreakpoint->bpsに追加する。