デバッガの実装(C/C++編)

デバッガの実装について興味があるので調べてみました。

まずは、基本であるC/C++(さらには、アセンブラ)のデバッガについて。

単語の定義

  • debugger

デバッグを行うプロセス。debuggeeの実行制御等を行う

  • debuggee

デバッグ対象のプロセス

解説

一般的なOS(Unix, Windows等)では、セキュリティを守るためなどの目的で、OS上で動作しているアプリケーションのデータをプロセスごとに独立した仮想アドレス上に配置し、
CPUのレジスタも個々のプロセス*1ごとに管理します。
このため、debuggerがdebuggeeのレジスタ、メモリの値を読んだり、書き変えたりするには、
OSの力を借りる必要があります。


また、debugeeの実行中に発生するハードウェア、ソフトウェア割り込みを元にOSが生成する非同期イベント
Unix/Linux系ではシグナル、Windowsでは構造化例外と呼ぶ。以下シグナル)の中には、
debuggeeが受け取る前に、debuggerが先に受け取って処理を行いたいシグナルが存在する*2ので、
debuggerはdebuggeeのシグナルをフックさせてくれとOSにお願いしなければいけません。
それ以外に、debuggeeの実行、終了、新規スレッドの追加、動的ライブラリのload, unloadなどのイベントも知りたいので、
これらのイベントもdebuggerに通知するようにOSにお願いする必要があります。


これらの依頼を行うために、OSから提供されるAPIデバッグAPIと呼びます。

デバッグAPIの例

  • Linux
    • ptrace関数*3(PTRACE_ME, PTRACE_ATTACH, PTRACE_POKEDATA, PTRACE_PEEKDATA, PTRACE_GETREGS, PTRACE_SETREGS, ...)
    • procファイルシステム
  • Windows
    • CreateProcess関数(の引数で渡す、DEBUG_PROCESSやDEBUG_ONLY_THIS_PROCESSフラグ)
    • DebugActiveProcess関数
    • Set(Get)ThreadContext関数(レジスタの値の読み書き)
    • Read(Write)ProcessMemory関数

たいていのOSのAPIは、

  1. debuggerがdebugeeプロセスを監視する権利を獲得し、debuggeeのシグナルやイベントの通知をOSから渡してもらえるようにする*4( PTRACE_ME, PTRACE_ATTACH, CreateProcess関数、DebugActiveProcess関数)
  2. レジスタやメモリの読み書きを行う

という手順で使われるようです。


ちなみに、各debuggeeの監視が行えるプロセスは1つしか許されないようで、
このことを利用したアンチデバッギング(プログラムの解析やチート行為などを回避するためにデバッグを防止すること)の手法がWizard Bible vol.32に書いてありました。
http://wizardbible.org/32/32.txt

breakpoint

breakpointには、主に2種類あります。

  • software breakpoint

debuggeeの中のbreakpointを仕掛けたい命令を、主にデバッグのために利用するCPUのソフトウェア割り込み命令(x86系CPUだとint 3命令)に書き換える。書き換えた割り込み命令が実行されたら、そのシグナルをdebuggerがキャッチして、仕掛けた割り込み命令を元の命令に書き換えてdebuggerのユーザの入力待ちに移行します

  • hardware breakpoint

CPUによっては、メモリのアドレスを格納し、このアドレスの命令やデータにアクセスする際に割り込みを発生させるCPUレジスタが存在する場合があります。これを使って割り込みを発生させ、その割り込みに対応するシグナルをdebuggerがキャッチして、debuggerのユーザの入力待ちに移行します

software breakpointの場合、debuggeeのメモリを書き換える必要があるのに対して、
hardware breakpointの場合、書き換える必要がないのがポイントです。

その他の機能

スタックトレースやステップ実行、ウォッチポイント等の機能は、レジスタやメモリの読み書きとbreakpointを応用することで実現できます。
C/C++ソースコードレベルでbreakpointを仕掛けたり、変数の値を見るためには、シンボルテーブル*5などを持つデバッグ情報(dwarf, stabs, coff等、複数の形式がある)が必要になります。

このあたりの話も書くと長くなるし、調べ切れてないので、いつか機会があったら書くことにします。


デバッガに関する話は、以下の本にかなり詳細に書かれてるので、興味を持った方は読んでみてください。

デバッガの理論と実装 (ASCII SOFTWARE SCIENCE Language)

デバッガの理論と実装 (ASCII SOFTWARE SCIENCE Language)

*1:正確には、プロセス内のスレッドごと

*2:たとえば、breakpointとして利用するソフトウェア割り込み(x86cpuの場合、int 3命令)

*3:関数じゃなくてシステムコールでした

*4:この段階で、debuggerはdebugeeの親プロセスになるようです(DebugActiveProcess関数を使ってアタッチした場合については、資料不足でよくわかりません・・・)

*5:変数名とそれに対応するアドレス情報のテーブル