ウェブサイト検索

GDBを使ったデバッグ: より深く掘り下げる


強力なGNUデバッガGDBが再び前面に登場します。私たちは、スタック、バックトレース、変数、コアダンプ、フレーム、デバッグについてこれまでになく深く掘り下げます。GDBへの新しく、より高度な紹介にぜひ参加してください。

GDBとは何ですか?

一般的にデバッグに不慣れな方、または特にGDB(GNUデバッガー)に不慣れな方は、まず「GDBでのデバッグ:始めに」という記事を読んでから、こちらの記事に戻ることをお勧めします。この記事では、そこで提示された情報をさらに深めていきます。

GDBのインストール

Debian/AptベースのLinuxディストリビューション(UbuntuやMintなど)にGDBをインストールするには、ターミナルで以下のコマンドを実行してください:

sudo apt install gdb

GDBをRedHat/YumベースのLinuxディストリビューション(RHEL、Centos、Fedoraなど)にインストールするには、ターミナルで次のコマンドを実行してください:

sudo yum install gdb

スタック、バックトレース、そしてフレーム!

リンゴ、パイ、パンケーキのようですね!(そして、ある程度そうです。)リンゴとパンケーキが私たちを養うように、スタック、バックトレース、フレームは、GDBでデバッグしているすべての開発者の基本です。そして、その中に提示される情報は、ソースコードのバグを見つけたいと切望する開発者を豊かに養います。

bt GDB コマンドは、呼び出されたすべての関数のバックトレースを生成し、次々に フレーム(関数)がリストされます。スタックは、クラッシュ、状況、または問題に至るまでの関数の概要またはリストであるため、バックトレースと非常に似ています。一方、バックトレースはスタックを得るために発行するコマンドです。

とはいえ、しばしば用語は互換的に使われ、一方で「スタックを取ってくれますか?」や「バックトレースを見てみましょう」と言うかもしれませんが、これはそれぞれの文の両方の単語の意味を多少逆転させます。

そして、前回の記事でのGDBの復習として、フレームは基本的にすべてのネストされた関数呼び出しのバックトレースにリストされている単一の関数です---例えば、

main()

最初に関数を開始し(スタックトレースの最後に表示される)、それから

main()

呼ばれる

math_function()

それが再び呼び出した

do_the_maths()

など

これが少し複雑に聞こえる場合は、まず「GDBでのデバッグ: 入門」を見てください。

シングルスレッドプログラムの場合、GDBは常に(または常にではない場合もありますが)デバッグを開始したときにクラッシュしている(そして唯一の)スレッドを正確に特定します。これにより、私たちが入るとすぐにbtコマンドを実行するのが簡単になります。

gdb

そして私たちはその場所にいます

(gdb)

プロンプトとして、GDBは私たちが観察したクラッシュに関連するバックトレースをすぐに表示してくれるでしょう。

シングルスレッドまたはマルチスレッドですか?

コアダンプのデバッグ時に観察すべき(知っておくべき)非常に重要な点は、デバッグしているプログラムが(あるいはより具体的に言うと、だった)単一スレッドかマルチスレッドかどうかですか?

前の例/記事では、自己作成のプログラムからのフレームのセットを示すシンプルなバックトレースを見ました。このプログラムはシングルスレッドであり、コード内から他の実行スレッドはフォークされませんでした。

しかし、複数のスレッドがある場合、単一のbt(バックトレース)コマンドは、GDBの中で現在選択されているスレッドのバックトレースのみを生成します。

GDBは自動的にクラッシュしているスレッドを選択し、マルチスレッドプログラムにおいても、99%以上の確率で正しく行われます。GDBがクラッシュしているスレッドを別のスレッドと間違えることは時々あります。例えば、プログラムが2つのスレッドで同時にクラッシュした場合にこれが起こることがあります。過去10年間で、数千のコアダンプを扱っている間にこれを目にしたのはほんの数回だけです。

私たちの前回の記事で使った例と真のマルチスレッドアプリケーションの違いを示すために、MariaDBのGitHubリポジトリにあるビルドスクリプトを使って、デバッグモード(言い換えれば、デバッグシンボル/計測が追加された状態)でMySQLサーバー8.0.25を構築し、それに対してpqueryフレームワークのSQLデータをしばらく実行した結果、すぐにMySQLデバッグサーバーがクラッシュしました。

以前の記事でお話ししたように、コアダンプとは、オペレーティングシステムまたは場合によってはアプリケーション自体(例外処理/コアダンプ機能が組み込まれている場合)が生成するファイルであり、そのファイルはGDBを使用して分析することができます。コアファイルは通常、メモリに含まれる機密情報を保護するために制限された権限のファイルとして書き込まれますので、スーパーユーザーアカウントを使用する必要があるかもしれません。

root

)にアクセスするため。

それでは、生成されたコアダンプにすぐに飛び込んでみましょう。

gdb bin/mysqld $(ls data/*core*)

:

そして数秒後、GDBは読み込みを終了し、私たちをGDBプロンプトに連れてきます。

さまざまなNew LWPメッセージ(フル出力ではさらに多数あります)は、このプログラムがマルチスレッドであることを示唆しています。LWPという用語はLight Weight Processの略です。これは、各スレッドに相当し、GDBがコアを分析している間に発見したすべてのスレッドのリストを作成することを意図しています。GDBは、クラッシュしているスレッドを見つけるために、これを前もって行う必要があることに注意してください。

また、上記の最初のGDBスタートアップ画像の最後の行に記載されているように、GDBは

Reading symbols from bin/mysqld

アクション。デバッグシンボルがバイナリに組み込まれていなかった場合、私たちは関数名でマークされたいくつか、またはほとんどのフレームを見ることができなかったでしょう。

??

さらに、これらの関数名に対して変数の読み出しは表示されません。

この問題(デバッグシンボルが取り除かれた最適化された/ストリップされたバイナリをデバッグしている時に見られる解決不可能なフレーム)は簡単には解決できません。たとえば、デバッグシンボルが取り除かれた生産レベルのデータベースサーバーのバイナリでこれを見た場合(ランタイムの最適化などのため)、一般的により複雑な手順に従う必要があります。たとえば、mysqldの完全なスタックトレースを生成する方法のように。

バックトレース!

デバッグシンボルを含むMySQLサーバーをコンパイルしたので、バックトレースは私たちのケースではすべての関数名を正しく表示します。私たちはbtコマンドを発行します。

(gdb)

プロンプトと、私たちのバックトレース出力は次のとおりです:

では、すべてのスレッドまたは別のスレッドのバックトレースをどのように見ることができますか?これは、コマンドを使用することで実現できます。

thread apply all bt

または

thread 2; bt

それぞれ。私たちは交換することができます。

2

最後のコマンドで別のスレッドにアクセスするなど。

thread apply all bt

ここに挿入するには出力が少し冗長です。別のスレッドに切り替えてそのスレッドのバックトレースを取得したときの出力は次のとおりです。

コンピュータのエラーログやトレースを注意深く読むことは、いつもそうであるように、情報をざっと見ただけでは見逃しがちな詳細を明らかにします。これは本当のスキルです。私の初期のITマネージャーの一人は、そうすることの大きな必要性を私に気づかせました。そして、この記事を熱心に読むすべての人に同じ情報を伝えます。その声明を裏付けるために、生成されたバックトレースを注意深く見てください。listen_for_connection_event、poll、Mysqld_socket_listener、およびconnection_event_loopという用語がMysqld_socket_listenerにあることに気付くでしょう。これは非常に明確です:このスレッドは入力を待っています。

これは、おそらくMySQLクライアントが接続するか、新しいコマンドを入力するのを待っているアイドルスレッドに過ぎません。言い換えれば、このスレッドをデバッグし続けることにはほとんど価値がないでしょう。

これにより、GDBが起動時にクラッシュしたスレッドを自動的に表示してくれる便利さに戻ります。デバッグの冒険を始めるために必要なことは、バックトレースを取得することだけです。それから、複数のスレッドとその相互作用を分析する際には、threadコマンドを使ってスレッド間をジャンプするのが理にかなっています。このコマンドは省略することもできます。

t

:

興味深いことに、ここでは私たちが持っています

thread 3

、いくつかの投票ループにも入っており、(のように見えます)

LinuxAIOHandler::poll

)ですが、この場合、それはOS/ディスクレベルでのことです(用語で示されているように)

Linux

,

AIO

、そして

Handler

)、そしてよく見ると、それは待っているようです。AIOの完了を待っているようです:

fil_aio_wait

ご覧のように、プログラムがクラッシュする瞬間の状態に関する情報がたくさんあります。それは、ログを注意深く見ることで確認できます。

ヒントがあります: あなたはこれを使うことができます。

set log on

GDBでコマンドを使って、後で出力を簡単に検索できるようにすべての情報をディスクに保存したい場合は、次のようにできます。

set log off

出力トレースを終了するため。情報は保存されています

gdb.txt

デフォルトで。

フレームに飛び込む

見た通り、スレッド間の切り替えが可能で、すべてのログのバックトレースを一度に取得することもできますし、個々のフレームに飛び込むことも可能です!さらに、ソースコードがディスク上にあり、製品をビルドする際に使用された元のディスクの場所(すなわち、同じソースコードディレクトリ)に保存されている場合、現在いる特定のフレームのソースコードを見ることもできます。

ここでは注意が必要です。バイナリ、コード、およびコアダンプの不一致は非常に簡単に起こります。例えば、特定のプログラムのバージョンv1.0で作成されたコアダンプを分析しようとすると、後でv1.01コードを用いてコンパイルされたバージョンv1.01のバイナリとは互換性がない可能性が高いです。また、v1.0のバイナリが利用可能であっても、v1.0のプログラムで書かれたコアダンプをデバッグするために、常にv1.01のソースコードを使用することはできません。

alwaysという言葉はオプションとして配置されました。なぜなら、時にはデバッグ中のプログラムのそのセクションのコードが前のバージョンから変更されていない場合、古いソースコードを使用できる可能性があるからです。

この慣行はおそらく好意的に見られていないでしょう。コードのいくつかの単純な変更が、コード行がバイナリやコアダンプと一致しなくなる可能性があるからです。異なるバージョンのソースコード、バイナリ、コアダンプを混ぜることは決して避けるべきか、またはソースコードを持たず、同じバージョンのコアダンプとバイナリのみに依存するべきです。ソースコードを手動で参照することは避けるべきです。

それでも、顧客から送られてきた多くのコアを解析している場合、情報が限られていることが多いため、ソースコードのわずかに異なるバージョンを使用しても問題ないことがあります。場合によっては、バイナリもわずかに異なることがあるかもしれません(可能性は低いですが)。ただし、GDBから表示される情報は、部分的に、あるいは全体的に無効である可能性が非常に高いことを常に認識してください。また、GDBは、コアとバイナリの間に不一致を検出できる場合、起動時に警告を表示します。

この例では、ソース、バイナリ、およびコアダンプはすべて同じバージョンのソースコードを使用しており、互いに作成されています。そのため、GDBがバックトレースのような出力を生成する際に、安心して信頼できます。

ここにはもう一つ小さな例外があります。それはスタック破損です。その場合、GDBでエラーメッセージを見るか、

??

フレーム名---上記の状況に似ている(ただし、今回はコアダンプの読み取り不能さとバイナリの組み合わせの結果として)---さもなければ、スタックは本当に奇妙で不正確に見えるでしょう。ほとんどの場合、非常に明確です。しかし、時には非常に悪いバグがスタック破損を引き起こすことがあります。

では、フレームに飛び込んで、いくつかの変数やコードがどのように見えるか見てみましょう:

変数

t 1

bt

f 7

frame 8

p *thd

p thd

ここでは、さまざまなコマンドを入力して正しいスレッドに移動し、バックトレースを実行しました(t 1は最初のスレッド、私たちの例でクラッシュしたスレッドに移動し、その後バックトレースコマンドbtを続けて実行しました)。その後、f7およびframe 8コマンドを使用して、それぞれフレーム7とフレーム8にジャンプしました。threadコマンドと同様に、frameコマンドを最初の文字fに略すことができる様子がわかります。

最後に、thd変数にアクセスしようとしましたが、この特定のフレームではトレース/コアダンプから最適化されていました。しかし、適切なフレームに飛び跳ねれば、変数が利用可能であり、最適化されていないため、その情報は利用可能です(少し試行錯誤が必要な場合があります):

上の最後の2つのスクリーンショットでは、print(再び、単にpとして略された)コマンドを入力する2つの異なる方法を示しました。最初の方法は変数名の前に*がついており、2番目の方法はついていません。

ここでの興味深い点は、2番目のものがより頻繁に使用されますが、一般的には問題の変数のメモリアドレスしか提供せず、それはあまり便利ではありません。コマンドの*バージョン(p *thd)は、変数をその全内容に解決します。また、GDBは変数の型を知っているので、型変換(値を別の変数型にキャストする)を行う必要はありません。

まとめる

このより詳細なGDBガイドでは、スタック、バックトレース、変数、コアダンプ、フレーム、デバッグについて探りました。いくつかのGDBの例を研究し、熱心な読者が効果的にデバッグするための重要なヒントをいくつか提供しました。この記事を楽しんでいただけたなら、私たちの「Linuxシグナルの仕組み:SIGINT、SIGTERM、SIGKILL」の記事もご覧ください。

関連記事: