プログラミング
デバッガをゼロから書く
Writing a Debugger from Scratch (timdbg.com)
要約
著者はRustをより深く学ぶため、そしてデバッガの仕組みを理解しやすくするために、独自のデバッガをゼロから作成しています。この記事では、Windows API(DebugActiveProcess、WaitForDebugEventExなど)を使用して、プロセスへのアタッチとデバッグイベントの監視という、デバッガの基本的な構造に焦点を当てています。
全文翻訳
私はMicrosoftのデバッガプラットフォームチームを2度離れましたが、そのたびに自分でデバッガを書き始めました。私はデバッガが本当に好きなのかもしれません。今回、新しいデバッガを書く理由は2つあります。1つ目は、Rustをより良く学びたいからです。すでにかなり理解しているものを作成するのは、学習の良い方法だと思います。2つ目の理由は、デバッガがどのように機能するかを人々が学びやすくすることです。Rustを使用することは、シンボルや逆アセンブリなどを処理できる多くのクレートがあり、デバッガ作成に関わる中心的なアイデアに集中できるため、ここでも役立ちます。
私はRust初心者です。もしあなたがRustをより良く学びたいのであれば、https://www.rust-lang.org/learn から始めることをお勧めします。Rustのコードについてあまり説明しようとはしませんが、主要な概念とAPIは言語を横断して適用できます。Rustの知識があまりなくても、ついていくことができるはずです。この記事を読んで、私よりもRustに詳しい方は、GitHubリポジトリで遠慮なくイシューを作成してください。さらに良いのは、プルリクエストを作成することです!この最初のパートのコードは「part1」というブランチにあり、完全なコードはここにあります。もしついていきたい場合は、リポジトリ全体をローカルにクローンし、rust-analyzerプラグインを備えたVS Codeのような優れたIDEで表示することをお勧めします。
デバッガとは何か?
まず、デバッガとは何でしょうか?「デバッグツール」と呼ばれるものはたくさんありますが、ほとんどの人が「デバッガ」と聞くと、実行中のシステムまたは静的なスナップショット(コアファイル、クラッシュダンプ、VMスナップショットなど)を分析できるツールを思い浮かべるでしょう。GDB、LLDB、Visual Studio、WinDbgのようなほとんどのデバッガは、これらの両方のことができます。私たちは、Windows上でライブユーザーモードデバッグをサポートするデバッガを作成することから始めます。中心的な概念はほとんどのOSに適用できますが、主な違いはプロセスをデバッグするためのOS APIと、特定の概念に使用される用語です。
Windowsには、デバッガの実装に必要な機能を提供するAPIがいくつかあります。これらのほとんどは、MSDNのデバッグ機能のページに記載されています。これらは、Visual StudioやWinDbgのようなデバッガが使用するAPIであることに注意してください。WinDbgのコアエンジンはDbgEngと呼ばれ、IDebugClientインターフェースおよび関連APIを通じてアクセスできる独自のAPIセットを持っています。これらの高レベルAPIは非常に強力で、スタックアンワインディング、シンボル解析、フロー制御、逆アセンブリ、スクリプティング、その他の多くの機能を内蔵しています。デバッグタスクを自動化したい場合は、こちらの方が良いAPIです。しかし、この投稿シリーズではそれを行うつもりはありません。私たちは、OSが提供する最も基本的なAPIを使用して、デバッガをゼロから実装します。
実行中のプロセスと対話するために必要な他の関数もいくつかありますが、これらのAPIはデバッグ固有のものではありません。それらは遭遇したときにカバーします。
基本的な構造
ライブデバッグセッションの中心はイベントループです。デバッガは、ターゲットプロセスに「アタッチ」することによって、ターゲットプロセスからのデバッグイベントを登録します。発生する各イベントに対して、OSはターゲットプロセスをフリーズし、デバッグイベントに関する情報でデバッガに通知します。デバッガはその後、ターゲットプロセスの状態を検査または操作する機会を得ます。デバッガはデバッグイベントから続行します。
ここには多くの複雑さが埋め込まれていますが、その複雑さのほとんどは、ターゲットプロセスの状態の検査と操作にあります。私たちは、プロセスにアタッチし、デバッグイベントを監視し、それらから続行してプロセスが正常に実行できるようにするデバッガを作成することから始めます。その部分は比較的簡単で、その上に構築できる他の機能の基盤を提供します。
プロセスへのアタッチ
Windowsでは、プロセスにアタッチするには主に2つの方法があります。デバッグしたい実行中のプロセスがすでに存在する場合、プロセスIDを唯一のパラメータとして受け取るDebugActiveProcess APIを使用できます。このAPI呼び出しの結果として、ハンドルやその他の情報は返されません。代わりに、ターゲットプロセスとデバッガプロセスの間に暗黙的な接続が作成されます。デバッグを停止するには、DebugActiveProcessStopを呼び出します。これもプロセスIDを唯一のパラメータとして受け取ります。
プロセスにアタッチする2番目の方法は、アタッチされた状態でプロセスを作成することです。それを行うには、通常デバッグに使用するのと同じCreateProcessW関数を使用し、dwCreationFlagsパラメータの一部としてDEBUG_ONLY_THIS_PROCESSまたはDEBUG_PROCESSフラグを含めます(プロセス作成フラグの一部として文書化されています)。これらのフラグの違いは、起動した特定のプロセスのみをデバッグしたいのか、それともそのプロセスとそのすべての子プロセスをデバッグしたいのかということです。
プロセスをデバッグするためにCreateProcessWを使用します。
105 let mut si: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
106 si.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
107 let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
108 let ret = unsafe {
109 CreateProcessW(
110 null(), // lpApplicationName
111 command_line_buffer.as_mut_ptr(), // lpCommandLine
112 null(), // lpProcessAttributes
113 null(), // lpThreadAttributes
114 FALSE, // bInheritHandles
115 DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE, // dwCreationFlags
116 null(), // lpEnvironment
117 null(), // lpCurrentDirectory
118 &mut si.StartupInfo, // lpStartupInfo
119 &mut pi, // lpProcessInformation
120
121 )
122 };
main.rs line 114
CreateProcessWには多くのパラメータがありますが、オプションのパラメータは一旦無視できます。後で、このデバッガのユーザーが特定の側面をカスタマイズできるようにしたいかもしれませんが、今のところは渡されたコマンドライン全体をlpCommandLineとして使用します。コマンドラインを起動する方法については、このプロジェクトのGitHubリポジトリのparse_command_lineを参照してください。また、dwCreationFlagsの一部としてDEBUG_ONLY_THIS_PROCESSフラグを渡します。CREATE_NEW_CONSOLEも指定しました。これにより、コンソールアプリをデバッグする際にコンソールを共有しなくなります(CDB/NTSDはターゲットとコンソールを共有し、私はそれを常に混乱し、ほとんど役に立たないと感じていました)。最後に、lpProcessInformationパラメータの一部として重要な情報を受け取ります。これは出力パラメータです。
CreateProcessWが返されると、プロセスは最初に中断された状態になります。デバッガプロセスがデバッグイベントの処理を開始するまで、コードは実行されません。それが次にすることです。
デバッグイベントの監視
Windowsでデバッガのイベントループを駆動するには、2つの関数が使用されます。1つ目はWaitForDebugEventEx関数です。2つ目の関数はContinueDebugEventで、イベントを処理した後にターゲットプロセスを再開します。これらはmain_debugger_loopから呼び出します。
56 loop {
57 let mut debug_event: DEBUG_EVENT = unsafe { std::mem::zeroed() };
58 unsafe {
59 WaitForDebugEventEx(&mut debug_event, INFINITE);
60 }
main.rs line 56
ここで興味深いのは、WaitForDebugEventEx関数がプロセス識別子を受け取らないことです。プロセスが複数のターゲットプロセスにアタッチされている場合、WaitForDebugEventExへのどの呼び出しも、すべてのアタッチされたプロセスのイベントを監視します。INFINITEパラメータは、デバッグイベントが受信されるまで無期限に待機することを意味します。イベントはDEBUG_EVENT構造体として記述され、スレッドID、プロセスID、およびイベントコードのフィールドがあります。各イベントタイプには独自のデータ構造があり、構造体は共用体として格納されます。共用体のどの構造体が有効かを判断するためにイベントコードを使用します。
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING