gdbの基本的なデバッグ原理は非常に単純であることがわかりました

I.はじめに

この記事では、有名なGDBについて説明します。その豊富な背景は言うまでもありません。兄弟のGCCと同様に、ゴールデンキー生まれ、GNUファミリーでの地位は揺るぎないものです。すべての組み込み開発エンジニアがgdbを使用してプログラムをデバッグしていると思います。gdbを使用していないと言った場合、それは開発経験が十分に粗くないことを意味するだけであり、BUGに打ち負かされ続ける必要があります。

gccでコンパイルするときに、-gオプションを使用して、実行可能ファイルにさらに多くのデバッグ情報を埋め込むことができることは誰もが知っています。では、どのデバッグ情報が埋め込まれているのでしょうか。このデバッグ情報はバイナリ命令とどのように相互作用しますか?デバッグ時に、デバッグ情報の関数呼び出しスタックのコンテキスト情報を取得するにはどうすればよいですか?

上記の疑問に応えて、ダオ兄弟は2つの記事を使用して、最下部の最も深い問題を徹底的に説明し、それらを一度に楽しむことができるようにしました。

最初の記事は現在のものです。主な内容は、GDBの基本的なデバッグ原理を紹介することです。GDBがデバッグされたプログラムの実行順序を制御するために使用するメカニズムを見てみましょう。

2番目の記事では、コンパクトで設備の整ったLUA言語を選択して、ソースコード分析から関数呼び出しスタック、命令セットからデバッグライブラリの変更まで、すべてを一度に分析します

より多くのコンテンツがあり、この記事を読み終えるのに時間がかかる場合があります。健康のため、しゃがんだ姿勢でこの記事を読むことはお勧めしません。

2、GDBデバッグモデル

GDBのデバッグには、gdbプログラムとデバッグされたプログラムの2つのプログラムが含まれます。これら2つのプログラムが同じコンピューターで実行されているかどうかに応じて、GDBのデバッグモデルは次の2つのタイプに分けることができます。

  1. ローカルデバッグ
  2. リモートデバッグ

ローカルデバッグ:デバッガーとデバッグされたプログラムは同じコンピューターで実行されます

リモートデバッグ:デバッグプログラムは1台のコンピューターで実行され、デバッグされたプログラムは別のコンピューターで実行されます

ビジュアルデバッグプログラムは重要ではなく、GDBをカプセル化するために使用されるシェルにすぎません。ダークターミナルウィンドウを使用してデバッグコマンドを手動で入力するか、IDEにデバッグ組み込まれている統合開発環境(IDE)を選択して、デバッグコマンドを手動で入力する代わりにさまざまなボタンを使用することもできます。

ローカルデバッグと比較して、リモートデバッグにはもう1つのGdbServerプログラムがあります。これとターゲットプログラムの両方が、x86コンピューターまたはARMボードなどターゲットマシン実行されます。図の赤い線は、ネットワークまたはシリアルポートを介したGDBとGdbServer間の通信を示しています。これは通信であるため、一連の通信プロトコルが必要です。RSPプロトコル、フルネームはGDBリモートシリアルプロトコル(GDBリモート通信プロトコル)です。

通信プロトコルの特定の形式と内容については、気にする必要はありません。知っておく必要があります。これらはすべて文字列であり、開始文字( '$')と終了文字( '#')が固定されています。最後に216。ベースのASCII文字がチェックサムとして使用され、多くのことを知っていれば十分です。詳細については、アイドル状態のXXを見ると、実際、これらの合意は、社会におけるあらゆる種類の奇妙な規制と同様に、トイレのレンガの束によってすべて考えられています。

LUAを説明する2番目の記事では、同様のリモートデバッグプロトタイプを実装します。通信プロトコルも文字列です。HTTPプロトコルを直接簡略化した後、使用されており、非常にわかりやすく便利です。

3、GDBデバッグ命令

完全を期すために、ここにいくつかのGDBデバッグコマンドを掲載しました。

さらに、すべての手順がここにリストされているわけではありません。リストされている手順はすべて一般的に使用されており、理解しやすいものです。LUAを説明するときは、基礎となる実装メカニズムを含め、詳細な比較のためにいくつかの手順を選択します。

各デバッグコマンドには多くのコマンドオプションがあります。たとえば、ブレークポイントにはブレークポイントの設定、ブレークポイントの削除、条件付きブレークポイント、一時的な無効化と有効化が含まれますこの記事の焦点は、gdbの基礎となるデバッグメカニズムを理解することであるため、アプリケーション層でのこれらの命令の使用法はリストされなくなりました。ネットワーク上には多くのリソースがあります。

第4に、GDBとデバッグされたプログラムの関係

説明の便宜上、最初に最も単純なCプログラムを作成します。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 2;
    int c = a + b;
    printf("c = %d \n", c);
    return 0;
}

コンパイルコマンド:

$ gcc -g test.c -o test

実行可能プログラムのテストをデバッグし、次のコマンドを入力します。

$ gdb ./test

出力は次のとおりです。

最後の行で、カーソルが点滅していることがわかります。これは、デバッグコマンドの発行を待機しているgdbプログラムです。

上記の暗いターミナルウィンドウがgdb./testを実行しているとき、オペレーティングシステムで多くの複雑なことが起こりました。

システムは最初にgdbプロセスを開始します。このプロセスはシステム関数fork()を呼び出して、子プロセスを作成します。この子プロセスは次の2つのことを行います。

  1. システム関数ptrace(PTRACE_TRACEME、[その他のパラメーター]);を呼び出します。
  2. 実行可能プログラムのテストは、execcを介してロードおよび実行され、テストプログラムはこのサブプロセスで実行を開始します。

もう1つのポイント:テキストではプログラムと呼ばれることもあれば、プロセスと呼ばれることもあります。「プログラム」は静的な概念、つまりハードディスク上にある一連のデータを表し、「プロセス」は動的なプロセスを表します。プログラムが読み取られてメモリにロードされた後、タスク制御ブロック(データ構造)は、このプロセスを管理するために特別に使用されます。

長い間基礎を築いた後、ついに主人公がデビューする番、つまりシステムコール関数ptrace(パラメータについては後で説明します)です。gdbが強力なデバッグ機能を備えているのはその助けを借りてです。関数プロトタイプは次のとおりです。

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

人間におけるこの関数の紹介を見てみましょう:

トレーサーは、 GDBのプログラムとして把握することができるデバッグプログラム、である。traceeは、図中の対象プログラムのテストに対応するデバッグプログラム、です。外国人は一般的に-erと-eeを使って能動的および受動的な関係を表現するのが好きです。たとえば、従業員は雇用主(上司)であり、従業員は強硬な雇用者(殴打労働者)です。

ptraceシステム関数は、Linuxカーネルによって提供されるプロセス追跡のシステムコールです。これにより、1つのプロセス(gdb)は、別のプロセス(テスト)の命令スペース、データスペース、スタック、およびレジスタ値を読み書きできます。また、gdbプロセスは、テストプロセスのすべてのシグナルを引き継ぎます。つまり、システムからテストプロセスに送信されたすべてのシグナルは、gdbプロセスによって受信されます。このように、テストプロセスの実行は、gdbによって制御されます。デバッグの目的を達成します。

つまり、gdbのデバッグがない場合、オペレーティングシステムとターゲットプロセスの間に直接の相互作用があります。gdbを使用してプログラムをデバッグすると、オペレーティングシステムからターゲットプロセスに送信される信号は、によってインターセプトされます。 gdb、およびgdbは、シグナルの属性に従って決定します。ターゲットプログラムの実行を続行する場合、現在インターセプトされているシグナルをターゲットプログラムに転送するかどうかをこのように、ターゲットプログラムはコマンドの下で対応するアクションを実行します。 gdbによって送信されたシグナル。

5、GDBが実行されたサービスプロセスをデバッグする方法

そのような質問を提起する小さなパートナーはいますか:上記のデバッグされたプログラムテストは最初から実行されますが、gdbを使用して、すでに実行されているサービスプロセスをデバッグできますか?答えは:はい。これには、ptraceシステム関数の最初のパラメーターが含まれます。このパラメーターは列挙値であり、PTRACE_TRACEMEとPTRACE_ATTACH <の2つが重要です

上記の説明では、子プロセスがptraceシステム関数を呼び出すために使用するパラメーターはPTRACE_TRACEMEです。オレンジ色のテキストに注意してください子プロセスはptraceを呼び出します。これは、子プロセスがオペレーティングシステムに言っているのと同じです。gdbプロセス私のお父さんです。シグナルを送信したい場合は、gdbプロセスに直接送信してください。

すでに実行されているプロセスBデバッグする場合はgdb親プロセスでptrace(PTRACE_ATTACH、[other parameters])呼び出す必要があります。このとき、gdbプロセスは実行されたプロセスBアタッチ(バインド)、gdbはを採用します。プロセスBはそれ自体の子プロセスであり、子プロセスBの動作はPTRACE_TRACEME操作と同等です。このとき、gdbプロセスはSIGSTOシグナルを子プロセスBに送信します。子プロセスBはSIGSTOPシグナルを受信すると、実行を一時停止してTASK_STOPED状態になり、デバッグの準備ができたことを示します。

したがって、ptraceシステムコールを介して、新しいプログラムまたはすでに実行中のサービスプログラムをデバッグする場合でも、最終結果は次のようになります。gdbプログラムが親プロセス、デバッグされたプログラムが子プロセス、およびすべてのシグナル子プロセスのすべてが親プロセスgdbに引き継がれ、親プロセスgdbは、スタック、レジスタなどを含む子プロセスの内部情報を表示および変更できます

バインドに関しては、理解する必要のあるいくつかの制限があります。自己バインドは許可されていません。同じプロセスへの複数のバインドは許可されていません。プロセスNo.1はバインドできません。

6、GDBがブレークポイント命令を実装する方法をのぞき見

原則は終わりました。ここでは、ブレークポイント(ブレーク)デバッグ命令設定して、 gdbの内部デバッグメカニズムを確認します。
それでも上記のコードを例として取り上げ、ここにコードを再投稿してください。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 2;
    int c = a + b;
    printf("c = %d \n", c);
    return 0;
}

コンパイルされた逆アセンブリコードがどのように見えるか、コンパイル命令を見てみましょう。

gcc -S test.c; 猫のテスト。S)

分解コードの一部のみをここに掲載します。基本的な原理を説明できる限り、私たちの目標は達成されます。

前述のように、gdb ./testを実行した後、gdbは子プロセスをforkします。この子プロセスは、最初にptraceを呼び出し、次にテストプログラムを実行して、デバッグ環境の準備を整えます。

理解しやすいように、ソースコードとアセンブリコードをまとめました。

デバッグウィンドウにブレークポイントコマンド「break5」を入力すると、gdbはこの時点で2つのことを実行します。

  1. ソースコードの5行目に対応するアセンブリコードの10行目は、ブレークポイントリンクリストに格納されます
  2. アセンブリコードの10行目に割り込み命令INT3を挿入します。これは、アセンブリコードの10行目がINT3に置き換えられることを意味します。

次に、デバッグウィンドウに実行コマンド「run」を入力し続けます(ブレークポイントに到達するまで実行して一時停止します)。アセンブリコードのPCポインタ(実行するコード行を指す内部ポインタ)が10行目で実行され、INT3命令であることが判明したため、オペレーティングシステムはSIGTRAP信号をテストプロセスに送信します。

この時点で、アセンブリコードの10行目が実行され、PCポインタが11行目を指しています。

上記のように、オペレーティングシステムからテストのために送信されたシグナルはすべてgdbに引き継がれます。つまり、gdbは最初にSIGTRAPシグナルを受信し、gdbは現在のアセンブリコードが10行目を実行していることを検出するため、ブレークポイントリストに移動します。 。検索では、コードの10行目がリンクリストに格納されていることがわかりました。これは、10行目にブレークポイントが設定されていることを示しています。したがって、gdbはさらに2つの操作を実行しました。

  1. アセンブリコードの10行目の「INT3」をブレークポイントリストの元のコードに置き換えます。
  2. PCポインタを1ステップ戻します。つまり、10行目を指すように設定します。

その後、gdbはユーザーのデバッグ指示を待ち続けます。

この時点で、次に実行される命令は、アセンブリコードの10行、つまりソースコードの5行目と同じですデバッガーの観点からは、デバッグ中のプログラム5行目のブレークポイントで一時停止しますこの時点で、変数値の表示、スタック情報の表示、ローカル変数値の変更など、デバッグする他のデバッグコマンドを引き続き入力できます。 、など。待ってください。

7つ目は、GDBが次のシングルステップ命令をどのように実装するかをのぞき見する

プログラムがソースコードの6行目、つまりアセンブリコードの11行目で停止すると仮定して、ソースコードとアセンブリコードを例として取り上げます。

次に、デバッグウィンドウでシングルステップ実行コマンドを入力します。目標は、コード行実行することです。つまり、ソースコードの6行目の実行を終了し、7行目で停止します。gdbは次の実行を受信すると、ソースコードの7行目を計算ます。これはアセンブリコードの14行目に対応する必要があるため、gdbアセンブリコードのPCポインターを制御して、13行目の終わりまで実行します。つまり、PCは停止時に最初の14行を指し、ユーザー入力のデバッグコマンドを待ち続けます。

8.まとめ

breakとnextの2つのデバッグ命令を通じて、gdbでデバッグ命令がどのように処理されるかを理解しました。もちろん、gdbには、スタック情報のより複雑な取得、変数値の変更など、さらに多くのデバッグ命令があります。興味のある友人は、引き続き詳細にフォローできます。

後でデバッグライブラリをLUA言語で作成するときに、この問題についてより詳細に説明します。結局のところ、LUA言語はより小さく、より単純です。また、LUAコードでPCポインターを設定する方法のコード部分も示します。これにより、プログラミング言語の内部実装をよりよく理解して把握し、ビデオを録画して、より良い結果を得ることができます。 LUA言語の内部の詳細を説明します。


この記事があなたに少しの助けをもたらすことができるならば、あなたの友人とコメントし、転送し、そして共有することを歓迎します。

パブリック・パブリックナンバーのIOTモノのインターネットタウンでの組み込みプロジェクトの開発プロセスにおける実際の戦闘経験引き続き要約します。失望することはないと思います。

おすすめ

転載: blog.csdn.net/u012296253/article/details/111150497