プログラマーであり、ソフトウェアに特定の機能を組み込みたい場合は、メソッドの作成、クラスの定義、新しいデータ型の作成など、それを実装する方法を考えることから始めます。次に、コンパイラーまたはインタープリターが理解できる言語で実装を記述します。しかし、すべてが正しく行われたと確信していても、コンパイラーまたはインタープリターが、意図したとおりに命令を理解しない場合はどうなるでしょうか。ソフトウェアはほとんどの場合正常に動作しますが、特定の状況でバグが発生する場合はどうなりますか?このような場合、問題の原因を見つけるためにデバッガーを正しく使用する方法を知っている必要があります。
GNU Project Debugger(GDB)は、プログラムのバグを見つけるための強力なツールです。実行中にプログラム内で何が起こっているかを追跡することで、エラーやクラッシュの理由を明らかにするのに役立ちます。
この記事は、基本的なGDBの使用法に関する実践的なチュートリアルです。例に従うには、コマンドラインを開いてこのリポジトリのクローンを作成します。
git clone https://github.com/hANSIc99/core_dump_example.git
その他のLinuxリソース
- Linuxコマンドのチートシート
- 高度なLinuxコマンドのチートシート
- 無料のオンラインコース:RHELの技術概要
- Linuxネットワーキングのチートシート
- SELinuxチートシート
- Linuxの一般的なコマンドのチートシート
- Linuxコンテナとは何ですか?
- 最新のLinux記事
GDBのすべてのコマンドを短縮できます。たとえば、info break
、設定されたブレークポイントを表示しますが、i break
に短縮できます。 。これらの略語は他の場所でも見られるかもしれませんが、この記事では、どの関数が使用されているかが明確になるように、コマンド全体を書き出します。
すべての実行可能ファイルにGDBをアタッチできます。クローンを作成したリポジトリに移動し、make
を実行してコンパイルします。 。これで、コアダンプという実行可能ファイルができました。 。 (Linuxダンプファイルの作成とデバッグに関する私の記事を参照してください。 詳細については..
GDBを実行可能ファイルにアタッチするには、次のように入力します。gdb coredump
。
出力は次のようになります:
デバッグシンボルが見つからなかったと表示されます。
デバッグ情報はオブジェクトファイル(実行可能ファイル)の一部であり、データ型、関数シグネチャ、およびソースコードとオペコードの関係が含まれます。この時点で、2つのオプションがあります。
- アセンブリのデバッグを続行します(以下の「記号なしのデバッグ」を参照)
- 次のセクションの情報を使用してデバッグ情報とコンパイルします
バイナリファイルにデバッグ情報を含めるには、それを再コンパイルする必要があります。 Makefileを開きます ハッシュタグを削除します(#
)9行目から:
CFLAGS =-Wall -Werror -std=c++11 -g
g
オプションは、デバッグ情報を含めるようにコンパイラーに指示します。 make clean
を実行します 続いてmake
GDBを再度呼び出します。この出力を取得して、コードのデバッグを開始できます:
追加のデバッグ情報により、実行可能ファイルのサイズが大きくなります。この場合、実行可能ファイルが2.5倍に増加します(26,088バイトから65,480バイトに)。
-c1
を使用してプログラムを開始します run -c1
と入力して切り替えます 。 State_4
に達すると、プログラムが起動してクラッシュします :
プログラムに関する追加情報を取得できます。コマンドinfo source
現在のファイルに関する情報を提供します:
- 101行
- 言語:C ++
- コンパイラ(バージョン、チューニング、アーキテクチャ、デバッグフラグ、言語標準)
- デバッグ形式:DWARF 2
- 使用可能なプリプロセッサマクロ情報はありません(GCCでコンパイルした場合、マクロは
-g3
でコンパイルした場合にのみ使用できます。 フラグ)。
コマンドinfo shared
プログラムが実行されるように、起動時にロードされた仮想アドレス空間にアドレスを含むダイナミックライブラリのリストを出力します。
Linuxでのライブラリ処理について知りたい場合は、私の記事Linuxで動的ライブラリと静的ライブラリを処理する方法を参照してください。 。
run
を使用してGDB内でプログラムを開始できることに気付いたかもしれません。 指図。 run
commandは、コンソールからプログラムを開始するために使用するのと同じように、コマンドライン引数を受け入れます。 -c1
スイッチを押すと、ステージ4でプログラムがクラッシュします。プログラムを最初から実行するために、GDBを終了する必要はありません。 run
を使用するだけです もう一度コマンドします。 -c1
なし スイッチを押すと、プログラムは無限ループを実行します。 Ctrl + Cで停止する必要があります 。
プログラムを段階的に実行することもできます。 C / C ++では、エントリポイントはmain
です。 働き。コマンドlist main
を使用します main
を示すソースコードの部分を開きます 機能:
main
関数は33行目にあるので、break 33
と入力してブレークポイントを追加します。 :
run
と入力してプログラムを実行します 。予想どおり、プログラムはmain
で停止します 働き。 layout src
と入力します ソースコードを並行して表示するには:
これで、GDBのテキストユーザーインターフェイス(TUI)モードになります。上矢印キーと下矢印キーを使用して、ソースコードをスクロールします。
GDBは、実行される行を強調表示します。 next
と入力する (n)、コマンドを1行ずつ実行できます。新しいコマンドを指定しない場合、GBDは最後のコマンドを実行します。コードをステップスルーするには、 Enterを押すだけです。 キー。
時々、TUIの出力が少し破損していることに気付くでしょう:
この場合は、 Ctrl + Lを押してください 画面をリセットします。
Ctrl + X + Aを使用します 自由にTUIモードに出入りします。他のキーバインディングはマニュアルにあります。
GDBを終了するには、「quit
」と入力するだけです。 。
このサンプルプログラムの中心は、無限ループで実行されているステートマシンで構成されています。変数n_state
現在の状態を決定する単純な列挙型です:
while(true){
switch(n_state){
case State_1:
std::cout << "State_1 reached" << std::flush;
n_state = State_2;
break;
case State_2:
std::cout << "State_2 reached" << std::flush;
n_state = State_3;
break;
(.....)
}
}
n_state
のときにプログラムを停止したい 値State_5
に設定されます 。これを行うには、main
でプログラムを停止します 関数を作成し、n_state
のウォッチポイントを設定します :
watch n_state == State_5
変数名を使用したウォッチポイントの設定は、目的の変数が現在のコンテキストで使用可能な場合にのみ機能します。
continue
と入力して、プログラムの実行を続行する場合 、次のような出力が得られるはずです:
実行を続行すると、ウォッチポイント式がfalse
と評価されたときにGDBが停止します。 :
一般的な値の変更、特定の値、および読み取りまたは書き込みアクセスの監視ポイントを指定できます。
info watchpoints
と入力します 以前に設定したウォッチポイントのリストを印刷するには:
ご覧のとおり、ウォッチポイントは数値です。特定のウォッチポイントを削除するには、delete
と入力します ウォッチポイントの番号が続きます。たとえば、私のウォッチポイントの番号は2です。このウォッチポイントを削除するには、delete 2
と入力します 。
注意: delete
を使用する場合 番号を指定せずに、すべて ウォッチポイントとブレークポイントは削除されます。
同じことがブレークポイントにも当てはまります。以下のスクリーンショットでは、いくつかのブレークポイントを追加し、info breakpoint
と入力してそれらのリストを印刷しました。 :
単一のブレークポイントを削除するには、delete
と入力します その番号が続きます。または、行番号を指定してブレークポイントを削除することもできます。たとえば、コマンドclear 78
78行目に設定されているブレークポイント番号7を削除します。
ブレークポイントまたはウォッチポイントを削除する代わりに、disable
と入力して無効にすることができます その番号が続きます。以下では、ブレークポイント3と4が無効になり、コードウィンドウでマイナス記号が付けられます。
disable 2 - 4
のように入力して、ブレークポイントまたはウォッチポイントの範囲を変更することもできます。 。ポイントを再度有効にする場合は、enable
と入力します その後に番号が続きます。
まず、delete
と入力して、すべてのブレークポイントとウォッチポイントを削除します 。それでも、プログラムをmain
で停止する必要があります 関数ですが、行番号を指定する代わりに、関数に直接名前を付けてブレークポイントを追加します。 break main
と入力します main
にブレークポイントを追加します 機能。
run
と入力します 最初から実行を開始すると、プログラムはmain
で停止します。 機能。
main
関数には変数n_state_3_count
が含まれています 、ステートマシンが状態3に達すると増分されます。
n_state_3_count
の値に基づいて条件付きブレークポイントを追加します タイプ:
break 54 if n_state_3_count == 3
実行を続行します。プログラムは、ステートマシンを3回実行してから、54行目で停止します。n_state_3_count
の値を確認するには 、タイプ:
print n_state_3_count
既存のブレークポイントを条件付きにすることもできます。 clear 54
で最近追加されたブレークポイントを削除します 、break 54
と入力して、簡単なブレークポイントを追加します 。次のように入力すると、このブレークポイントを条件付きにすることができます。
condition 3 n_state_3_count == 9
3
ブレークポイント番号を参照します。
複数のソースファイルで構成されるプログラムがある場合は、行番号の前にファイル名を指定することでブレークポイントを設定できます(例:break main.cpp:54
)。 。
ブレークポイントとウォッチポイントに加えて、キャッチポイントを設定することもできます。キャッチポイントは、システムコールの実行、共有ライブラリの読み込み、例外の発生などのプログラムイベントに適用されます。
write
をキャッチするには STDOUTへの書き込みに使用されるsyscallは、次のように入力します。
catch syscall write
プログラムがコンソール出力に書き込むたびに、GDBは実行を中断します。
マニュアルには、ブレークポイント、ウォッチポイント、キャッチポイントをカバーする章全体が記載されています。
変数の値の印刷は、print
を使用して行われます。 指図。一般的な構文はprint <expression> <value>
です。 。変数の値は、次のように入力して変更できます。
set variable <variable-name> <new-value>.
以下のスクリーンショットでは、変数n_state_3_count
を指定しました 値123 。
/x
expressionは、値を16進数で出力します。 &
で 演算子を使用すると、仮想アドレス空間内のアドレスを印刷できます。
特定のシンボルのデータ型がわからない場合は、whatis
で見つけることができます。 :
main
のスコープで使用可能なすべての変数を一覧表示する場合 関数、「info scope main
」と入力します :
DW_OP_fbreg
値は、現在のサブルーチンに基づくスタックオフセットを参照します。
または、すでに関数内にいて、現在のスタックフレーム上のすべての変数を一覧表示する場合は、info locals
を使用できます。 :
記号の検査の詳細については、マニュアルを確認してください。
コマンドgdb attach <process-id>
プロセスID(PID)を指定することにより、すでに実行中のプロセスに接続できます。幸い、coredump
プログラムは現在のPIDを画面に出力するため、psまたはtopを使用して手動で見つける必要はありません。
コアダンプアプリケーションのインスタンスを開始します:
./coredump
オペレーティングシステムはPID2849
を提供します 。別のコンソールウィンドウを開き、コアダンプアプリケーションのソースディレクトリに移動して、GDBをアタッチします。
gdb attach 2849
GDBを接続すると、GDBはすぐに実行を停止します。 layout src
と入力します およびbacktrace
コールスタックを調べるには:
出力には、std::this_thread::sleep_for<...>(...)
の実行中に中断されたプロセスが表示されます。 main.cpp
の92行目で呼び出された関数 。
GDBを終了するとすぐに、プロセスは実行を継続します。
実行中のプロセスへのアタッチの詳細については、GDBのマニュアルを参照してください。
up
を使用してプログラムに戻ります スタック内をmain.cpp
に移動するために2回 :
通常、コンパイラは関数またはメソッドごとにサブルーチンを作成します。各サブルーチンには独自のスタックフレームがあるため、スタックフレーム内で上に移動すると、コールスタック内で上に移動することになります。
スタック評価の詳細については、マニュアルをご覧ください。
すでに実行中のプロセスにアタッチする場合、GDBは現在の作業ディレクトリでソースファイルを探します。または、directory
を使用してソースディレクトリを手動で指定することもできます。 コマンド。
Linuxダンプファイルの作成とデバッグをお読みください このトピックについての情報。
TL; DR:
- 最新バージョンのFedoraを使用していると思います
- c1スイッチを使用してコアダンプを呼び出します:
coredump -c1
- 最新のダンプファイルをGDBでロードします:
coredumpctl debug
- TUIモードを開き、
layout src
に入ります。
backtrace
の出力 クラッシュがmain.cpp
から5スタックフレーム離れた場所で発生したことを示しています 。入力して、main.cpp
の誤ったコード行に直接ジャンプします :
ソースコードを見ると、プログラムがメモリ管理関数によって返されないポインタを解放しようとしたことがわかります。これにより、未定義の動作が発生し、SIGABRT
が発生しました 。
利用可能なソースがない場合、事態は非常に困難になります。リバースエンジニアリングの課題を解決しようとしたときに、これを初めて経験しました。アセンブリ言語の知識があると便利です。
この例でどのように機能するかを確認してください。
ソースディレクトリに移動し、 Makefileを開きます 、9行目を次のように編集します:
CFLAGS =-Wall -Werror -std=c++11 #-g
プログラムを再コンパイルするには、make clean
を実行します 続いてmake
GDBを起動します。プログラムには、ソースコードを導くためのデバッグシンボルがありません。
コマンドinfo file
バイナリのメモリ領域とエントリポイントを明らかにします:
エントリポイントは、.text
の先頭に対応します エリア。実際のオペコードが含まれています。エントリポイントにブレークポイントを追加するには、break *0x401110
と入力します。 次に、run
と入力して実行を開始します :
特定のアドレスにブレークポイントを設定するには、間接参照演算子*
を使用してブレークポイントを指定します 。
アセンブリを深く掘り下げる前に、使用するアセンブリフレーバーを選択できます。 GDBのデフォルトはAT&Tですが、私はIntel構文を好みます。次のように変更します:
set disassembly-flavor intel
次に、アセンブリを開き、layout asm
と入力してウィンドウを登録します。 およびlayout reg
。次のような出力が表示されます:
すでに多くのコマンドを入力していますが、実際にはデバッグを開始していません。アプリケーションを頻繁にデバッグしている場合、またはリバースエンジニアリングの課題を解決しようとしている場合は、GDB固有の設定をファイルに保存すると便利です。
設定ファイルgdbinit
このプロジェクトのGitHubリポジトリには、最近使用したコマンドが含まれています:
set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg
set write on
コマンドを使用すると、実行中にバイナリを変更できます。
GDBを終了し、構成ファイルを使用して再度開きます:gdb -x gdbinit coredump
。
c2
を使用 スイッチを適用すると、プログラムがクラッシュします。プログラムはエントリ関数で停止するため、continue
と記述する必要があります。 実行を続行するには:
idiv
命令は、RAX
の被除数を使用して整数除算を実行します レジスタと引数として指定された除数。商はRAX
にロードされます 登録すると、残りはRDX
にロードされます 。
レジスターの概要から、RAX
を確認できます。 5が含まれています 、したがって、スタックのRBP-0x4
の位置に格納されている値を確認する必要があります。 。
生のメモリコンテンツを読み取るには、シンボルの読み取りよりもいくつかのパラメータを指定する必要があります。アセンブリ出力を少し上にスクロールすると、スタックの分割がわかります。
rbp-0x4
の値に最も関心があります これは、idiv
の引数が存在する位置だからです。 保存されています。スクリーンショットから、次の変数がrbp-0x8
にあることがわかります。 、したがって、rbp-0x4
の変数 幅は4バイトです。
GDBでは、x
を使用できます 調べるコマンド 任意のメモリコンテンツ:
x/
<オプションのパラメータn
f
u
><メモリアドレスaddr
>
オプションのパラメータ:
-
n
:繰り返し回数(デフォルト:1)はユニットサイズを指します -
f
:printfのようなフォーマット指定子 -
u
:ユニットサイズ-
b
:バイト -
h
:ハーフワード(2バイト) -
w
:ワード(4バイト)(デフォルト) -
g
:ジャイアントワード(8バイト)
-
rbp-0x4
の値を出力するには 、「x/u $rbp-4
」と入力します :
このパターンを念頭に置いておくと、メモリを調べるのは簡単です。マニュアルのメモリの検査セクションを確認してください。
サブルーチンzeroDivide()
で算術例外が発生しました 。上矢印キーを使用して少し上にスクロールすると、次のパターンが見つかります。
0x401211 <_Z10zeroDividev> push rbp
0x401212 <_Z10zeroDividev+1> mov rbp,rsp
これは関数プロローグと呼ばれます:
- ベースポインタ(
rbp
)呼び出し元の関数はスタックに格納されます - スタックポインタの値(
rsp
)がベースポインタ(rbp
)にロードされます )
このサブルーチンを完全にスキップします。 backtrace
を使用してコールスタックを確認できます 。 main
のスタックフレームは1つだけ進んでいます 関数なので、main
に戻ることができます 単一のup
:
main
関数、あなたはこのパターンを見つけることができます:
0x401431 <main+497> cmp BYTE PTR [rbp-0x12],0x0
0x401435 <main+501> je 0x40145f <main+543>
0x401437 <main+503> call 0x401211<_Z10zeroDividev>
サブルーチンzeroDivide()
jump equal (je)
の場合にのみ入力されます true
と評価されます 。これはjump-not-equal (jne)
に簡単に置き換えることができます オペコード0x75
を持つ命令 (x86 / 64アーキテクチャを使用している場合、オペコードは他のアーキテクチャとは異なります)。 run
と入力して、プログラムを再起動します 。プログラムが入力機能で停止したら、次のように入力してオペコードを操作します。
set *(unsigned char*)0x401435 = 0x75
最後に、continue
と入力します 。プログラムはサブルーチンzeroDivide()
をスキップします もうクラッシュしません。
GDBは、Qt CreatorやVSCodiumのネイティブデバッグ拡張機能など、多くの統合開発環境(IDE)でバックグラウンドで動作していることがわかります。
GDBの機能を活用する方法を知っておくと便利です。通常、GDBのすべての機能をIDEから使用できるわけではないため、コマンドラインからGDBを使用した経験があると便利です。