Rubyが好きなのにRubyよりCやC++を書いている時間の方が長い須藤です。Cで書かれたプログラムのデバッグに便利なaddr2line
をデバッグ情報が分離されたファイルに対して使う方法を説明します。
addr2line
まず、前提知識としてaddr2line
について説明します。あれ?説明するか、と思ってみたもののあんまり詳しく知らない気がするな。。。まぁ、私が知っている範囲で説明します。
addr2line
はGNU Binutilsに含まれているプログラムの1つで、アドレスを元に、それに対応するソースコードの位置を特定する便利プログラムです。といっても、これでピンとくる人はいないでしょう。どの「アドレス」を元にするの?というところがピンとこないと思います。
多くの場合、addr2line
に与えるアドレスはglibcのバックトレース用関数で出力したバックトレース内に含まれています。というか、私がaddr2line
を使うときはこのバックトレースで取得できたアドレスしか使っていません。他の人がどうやって使っているのか知りませんが、これが代表的な使い方じゃないかな。
たとえば、glibcのバックトレース用関数を使うと次のような情報を得られます。これはGroongaが実際に出力したログから抜粋したものです。
/lib/x86_64-linux-gnu/libgroonga.so.0(+0xe68ad) [0x79d5ad2e68ad]
この中の+0xe68ad
の部分がaddr2line
に渡すアドレスになります。これを渡すと「XXX.c
の29行目だよ」のように教えてくれます。C言語がスクリプト言語よりデバッグが難しい原因の1つがバックトレースを取得しにくい(クラッシュした箇所を特定しにくい)ことですが、(glibcのバックトレース用関数と)addr2line
を使うことでそれを解消することができます。便利そうでしょ?
デバッグ情報
では、addr2line
はどうやってアドレスから該当するソースコードの位置を特定しているのでしょうか。あぁ、これも私は雰囲気でしかわかっていないですね。私がこんな感じで動いているんじゃないかな?と思っていることを説明しましょう。(違ったら教えて。)
addr2line
はアドレスを元にソースコードの位置を特定していると説明しましたが、実は入力はアドレスだけではありません。アドレスとバイナリーです。バイナリーのフォーマットはLinuxではELF(Executable and Linkable Format、エルフ)です。
ELFのバイナリーにはデバッグ情報を埋め込むことができ、その情報の中にソースコードの情報も含まれています。addr2line
はデバッグ情報とアドレスを使い、対応するソースコードの位置を特定しています。
そのため、デバッグ情報なしでビルドしたバイナリーからはソースコードの位置を特定することができません。デバッグ情報をつけるには、たとえば、GCCなら-g3
オプションを使います。
あわせて読みたい:GDBでデバッグするなら-g3
オプション
しかし、デバッグ情報をバイナリーに埋め込むとすごくサイズが大きくなることが多いです。バイナリー本体よりデバッグ情報の方が大きいこともよくあります。そのため、本番環境でデバッグ情報入りのバイナリーを使うことがためらわれたりします。そうなると、本番環境でしかクラッシュしない問題の調査が大変です。どうしよう!
デバッグ情報の分離
実はバイナリーからデバッグ情報を分離して別ファイルとして持つことができます。これでなにがうれしいかというと、本番環境ではデバッグ情報なしの小さなバイナリーを使うことができるのです!デバッグ情報なしのバイナリーで出力されたバックトレース内のアドレスでも、分離されたデバッグ情報を使って対応するソースコードの位置を特定できます。デバッグ時には本番環境とは別のデバッグ環境を用意して、そこにだけデバッグ情報をインストールします。そのデバッグ環境でデバッグ情報とaddr2line
を使えば、本番環境にデバッグ情報がなくてもソースコードの位置を特定できます。
ということで、前提知識の説明はこれでおしまい。それでは、どうやってデバッグ情報が分離されたバイナリーに対してaddr2line
を使うのかを説明します。
デバッグ情報が分離されたバイナリーとaddr2line
バイナリーにデバッグ情報が含まれている場合は次のようにすればソースコードの位置を特定できます。
addr2line --exe=/lib/x86_64-linux-gnu/libgroonga.so.0 +0xe68ad
対象のバイナリーとアドレスを指定するだけです。簡単ですね。
デバッグ情報が分離されている場合は--exe
に対象のバイナリーではなくデバッグ情報が分離されたファイルを指定します。では、分離されたファイルはどこにあるのでしょうか。
debパッケージの場合は最後に-dbgsym
がついた名前のパッケージにデバッグ情報が分離されたファイルが入っています。Groongaの場合はlibgroonga0-dbgsym
です。
RPMパッケージの場合は最後に-debuginfo
がついた名前のパッケージにデバッグ情報が分離されたファイルが入っています。Groongaの場合はgroonga-libs-debuginfo
です。
たとえば、Debian GNU/Linuxではこのパッケージをインストールすると/usr/lib/debug/
以下にデバッグ情報が分離されたファイルがインストールされます。たとえば、Groonga 14.1.2のlibgroonga.so
から分離されたデバッグ情報は次のパスにあります。
/usr/lib/debug/.build-id/40/4433eafc49c567b38076510498c1a2b54efb17.debug
パスだけ見ても対応しているなんて全然わからないですよねー
でも、これ、意味のないランダムな値というわけでもないのです。なにかというと、Build IDです。Build IDの詳細も私はよくわかっていないので私がわかっている範囲で説明しますが、ビルドしたバイナリーにIDを埋め込んだものです。どうやって計算しているかを調べたことはないのですが、同じビルド方法なら同じビルドIDになるんじゃないかと思っています。(Reproducible Buildsと共存できるんだと思っています。もろもろReproducible Builds対応しないとなぁと思いつつまだやっていないので、まだまじめに調べていません。)
ということで、バイナリーに埋め込まれたBuild IDを知ることができればデバッグ情報を分離したファイルの場所がわかります。
では、どうすればBuild IDを知ることができるのでしょうか。私が知っている方法はreadelf
を使う方法です。もっとスマートな方法があるんじゃないかとは思うのですが、私は次のようにしてBuild IDを調べています。
まず、readelf --notes
で.notes
セクションを出力します。
$ readelf --notes /usr/lib/x86_64-linux-gnu/libgroonga.so
Displaying notes found in: .note.gnu.build-id
Owner Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: 404433eafc49c567b38076510498c1a2b54efb17
この中のBuild ID:
の後の文字列がBuild IDです。先頭2文字が/usr/lib/debug/.build-id/
直下のディレクトリー名に使われるので、次のようになります。
/usr/lib/debug/.build-id/40/
この下に残りの文字列が入って、最後に.debug
がつきます。
/usr/lib/debug/.build-id/40/4433eafc49c567b38076510498c1a2b54efb17.debug
これをaddr2line
で使います。
addr2line --exe=/usr/lib/debug/.build-id/40/4433eafc49c567b38076510498c1a2b54efb17.debug +0xe68ad
これでデバッグ情報が分離されていても対応するソースコードの位置を特定できます。ちなみに、この.debug
ファイルのフォーマットもELFです。このELFの中にデバッグ情報が入っています。ちなみに、デバッグ情報が使っているフォーマットの名前はDWARF(ドワーフ)です。
なお、Groongaのバグレポートのたびにこのようなことを手動でやるのは面倒なのでスクリプトを用意して自動化しています。このスクリプトは対象の環境内で動かさないといけない(たとえば、Debian GNU/Linux bookwormのバックトレースの場合はDebian GNU/Linux bookwormでこのスクリプトを動かさないといけない)のですが、それを用意するのは面倒なので、Dockerで対象環境を作ってこのスクリプトを動かすラッパースクリプトも用意しています。
まとめ
GroongaのバックトレースからGroongaの対応するソースコードの位置を特定する方法を知らない人がいたのでやり方をまとめました。Groonga以外でも活用できる便利情報なのでC言語で書かれた実行ファイル・ライブラリーがクラッシュしてglibcのバックトレース出力がある場合は活用してください。