ククログ

株式会社クリアコード > ククログ > デバッグ情報が分離されていてもaddr2lineでソースコードの位置を特定する方法

デバッグ情報が分離されていてもaddr2lineでソースコードの位置を特定する方法

Rubyが好きなのにRubyよりCやC++を書いている時間の方が長い須藤です。Cで書かれたプログラムのデバッグに便利なaddr2lineをデバッグ情報が分離されたファイルに対して使う方法を説明します。

addr2line

まず、前提知識としてaddr2lineについて説明します。あれ?説明するか、と思ってみたもののあんまり詳しく知らない気がするな。。。まぁ、私が知っている範囲で説明します。

addr2lineGNU 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のバックトレース出力がある場合は活用してください。