###導入###
| ついにこのシリーズも最後の記事になりました!今回は、デバッグにおけるより高度な概念 (リモート デバッグ、共有ライブラリのサポート、式の評価、およびマルチスレッドのサポート) の概要を説明します。これらのアイデアは実装がより複雑なので、その方法については詳しく説明しませんが、これらのコンセプトについてご質問があれば喜んでお答えします。
|
シリーズインデックス
- 環境の準備
- ブレークポイント
- レジスタとメモリ
- エルフとドワーフ
- ソースコードとシグナル
- ソースコード層は段階的に実行されます
- ソースコード層ブレークポイント
- コールスタック
- 変数の処理
- 高度なテーマ
リモートデバッグ
リモート デバッグは、組み込みシステムやさまざまな環境のデバッグに非常に役立ちます。また、高レベルのデバッガ操作と、オペレーティング システムおよびハードウェアとの対話との間に紙一重の境界線を設定します。実際、GDB や LLDB などのデバッガは、ローカル プログラムをデバッグする場合でもリモート デバッガとして実行できます。一般的なアーキテクチャは次のとおりです:
デブアーカイブ
デバッガーは、コマンド ラインを通じて対話するコンポーネントです。おそらく IDE を使用している場合は、その上にマシン インターフェイスを介してデバッガーと通信する別のレイヤーがあるでしょう。ターゲット マシン (おそらくネイティブ マシンと同じ) にはデバッグ スタブがあります。これは理論的には、アドレスにブレークポイントを設定するなどのすべての低レベルのデバッグ タスクを実行する、非常に小さなオペレーティング システム デバッグ ライブラリのラッパーです。 「理論上」と言ったのは、最近デバッグ スタブがますます大きくなっているためです。たとえば、私のマシンの LLDB デバッグ スタブのサイズは 7.6MB です。デバッグ スタブは、オペレーティング システム固有の機能 (この場合は ptrace) を使用して、リモート プロトコル経由でデバッグ対象のプロセスおよびデバッガーと通信します。
最も一般的なリモート デバッグ プロトコルは、GDB リモート プロトコルです。これは、デバッガとデバッグ スタブの間でコマンドと情報を渡すために使用されるテキストベースのパケット形式です。詳細については説明しませんが、ここでさらに詳しく読むことができます。 LLDB を起動し、コマンド log enable gdb-remote packets を実行すると、リモート プロトコル経由で送信されたすべてのパケットのトレースが取得されます。 GDB では、setremotelogfile を使用して同じことを行うことができます。
簡単な例として、これはブレークポイントを設定するパケットです:
リーリー
$ パケットの先頭をマークします。 Z0 はメモリ ブレークポイントを挿入するコマンドです。 400570 と 1 はパラメータで、前者はブレークポイントを設定するアドレス、後者は特定のターゲットのブレークポイント タイプ指定子です。最後に、#43 はデータが破損していないことを確認するためのチェックサムです。
GDB リモート プロトコルは、カスタム パケットを使用して拡張するのが非常に簡単で、プラットフォームまたは言語固有の機能を実装するのに役立ちます。
共有ライブラリと動的読み込みのサポート
デバッガは、ブレークポイントを設定したり、ソース コード レベルの情報やシンボルを取得したりできるように、デバッグ中のプログラムによってどの共有ライブラリがロードされているかを知る必要があります。デバッガーは、動的にリンクされているライブラリを検索するだけでなく、実行時に dlopen を介してロードされるライブラリもトレースする必要があります。この目的を達成するために、ダイナミック リンカーは交差構造を維持します。この構造は、共有ライブラリ記述子のリンク リストと、リンク リストが更新されるたびに呼び出される関数へのポインタを維持します。この構造体は ELF ファイルの .dynamic セクションに保存され、プログラムの実行前に初期化されます。
シンプルな追跡アルゴリズム:
- トレーサは、ELF ヘッダー内のプログラムのエントリを検索します (または、/proc//aux に保存されている補助ベクトルを使用することもできます)。
- 追跡プログラムは、プログラムの入り口にブレークポイントを設定し、実行を開始します。
- ブレークポイントに到達すると、ELF ファイル内の .dynamic のロード アドレスを検索することで交差構造のアドレスが見つかります。
- 現在ロードされているライブラリのリストについては、交差構造を確認してください。
- リンカ更新関数にブレークポイントを設定します。
- リストはブレークポイントに到達するたびに更新されます。
- 追跡プログラムは無限ループし、プログラムの実行を継続し、追跡プログラム信号が終了するまで信号を待ちます。
これらの概念の小さな例を書きました。ここにあります。興味のある方がいらっしゃいましたら、今後詳しく書きます。
式の計算
式の評価は、ユーザーがプログラムのデバッグ中に元のソース言語で式を評価できるようにするプログラムの機能です。たとえば、LLDB または GDB では、print foo() を実行して foo 関数を呼び出し、結果を出力できます。
式の複雑さに応じて、いくつかの異なる計算方法があります。式が単なる単純な識別子の場合、このシリーズの最後の部分で行ったのと同じように、デバッガーはデバッグ情報を調べ、変数を見つけて値を出力できます。式がある程度複雑な場合は、コードを中間式 (IR) にコンパイルし、それを解釈して結果を取得できる場合があります。たとえば、一部の式の場合、LLDB は Clang を使用して式を LLVM IR にコンパイルし、解釈します。式がより複雑な場合、または特定の関数の呼び出しが必要な場合は、コードをターゲットに JIT してデバッグ対象のアドレス空間で実行する必要がある場合があります。これには、mmap を呼び出して実行可能メモリを割り当て、コンパイルされたコードをそのブロックにコピーして実行することが含まれます。 LLDB は、LLVM の JIT 機能を使用して実装されます。
JIT コンパイルについてさらに詳しく知りたい場合は、このテーマに関する Eli Bendersky の記事を強くお勧めします。
多线程调试支持
本系列展示的调试器仅支持单线程应用程序,但是为了调试大多数真实程序,多线程支持是非常需要的。支持这一点的最简单的方法是跟踪线程的创建,并解析 procfs 以获取所需的信息。
Linux 线程库称为 pthreads。当调用 pthread_create 时,库会使用 clone 系统调用来创建一个新的线程,我们可以用 ptrace 跟踪这个系统调用(假设你的内核早于 2.5.46)。为此,你需要在连接到调试器之后设置一些 ptrace 选项:
ptrace(PTRACE_SETOPTIONS, m_pid, nullptr, PTRACE_O_TRACECLONE);
现在当 clone 被调用时,该进程将收到我们的老朋友 SIGTRAP 信号。对于本系列中的调试器,你可以将一个例子添加到 handle_sigtrap 来处理新线程的创建:
case (SIGTRAP | (PTRACE_EVENT_CLONE << 8)):
//get the new thread ID
unsigned long event_message = 0;
ptrace(PTRACE_GETEVENTMSG, pid, nullptr, message);
//handle creation
//...
一旦收到了,你可以看看 /proc//task/ 并查看内存映射之类来获得所需的所有信息。
GDB 使用 libthread_db,它提供了一堆帮助函数,这样你就不需要自己解析和处理。设置这个库很奇怪,我不会在这展示它如何工作,但如果你想使用它,你可以去阅读这个教程。
多线程支持中最复杂的部分是调试器中线程状态的建模,特别是如果你希望支持不间断模式或当你计算中涉及不止一个 CPU 的某种异构调试。
最后!
呼!这个系列花了很长时间才写完,但是我在这个过程中学到了很多东西,我希望它是有帮助的。如果你有关于调试或本系列中的任何问题,请在 Twitter @TartanLlama或评论区联系我。如果你有想看到的其他任何调试主题,让我知道我或许会再发其他的文章。