/usr/lib/.build-idってなんだろう?

Red Hatの森若です。

rpmパッケージに含まれるファイル一覧を rpm -ql glibc のようにして表示すると、/usr/lib/.build-id ディレクトリ以下に多数のファイルがあることに気付きます。 「このファイルは何ですか?」 という素朴な疑問に答えるため、背景知識をふくめて紹介していきます。

$ rpm -ql glibc
(中略)
/lib64/librt.so.1
/lib64/libthread_db.so.1
/lib64/libutil.so.1
/sbin/ldconfig
/usr/lib/.build-id
/usr/lib/.build-id/03
/usr/lib/.build-id/03/f4fad34e4c1a28712ec68f3c106fc200e25c60
/usr/lib/.build-id/06
/usr/lib/.build-id/06/3f74dd93e6bd487beebd93975dd7d6a75e7c4d
/usr/lib/.build-id/06/f56ed55884abd425ff0fd12872bf3fae0793b8
/usr/lib/.build-id/0f
/usr/lib/.build-id/0f/90095799730a30406eb56af85830689679b356
/usr/lib/.build-id/1e
/usr/lib/.build-id/1e/acb7c50f7ed20ef1fefda3aa9c67377686acf5
/usr/lib/.build-id/22
/usr/lib/.build-id/22/f2295089fd568f1ec1ff6f40c51b3ef4dd528a
/usr/lib/.build-id/31
/usr/lib/.build-id/31/9dab886fc7c7c8bc15b4aa33ed5b08fb84a579
(以下略)

TL;DR

  • /usr/lib/.build-id にあるシンボリックリンクはELFファイル(実行ファイルや共有ライブラリ、カーネルモジュール)に含まれる Build IDに対応している
  • Build IDはELFファイルに格納されているIDで、内容のハッシュ値。
  • Build IDの用途は大きくわけて2つ。/usr/lib/.build-idは後者のために利用されている。

    • デバッグに必要な情報を提供する debuginfo パッケージ内のファイルと実行用のELFファイルが対応していることの確認
    • コアダンプに保存したBuild IDからELFファイルを検索
  • RHEL 7までは /usr/lib/.build-id に相当するファイルは debuginfo パッケージに含まれていた

  • RHEL 8以降は、複数バージョンのdebuginfoパッケージをインストールできるように通常のrpmにbuild-idが含まれるようになった
    • 複数バージョンのdebuginfoパッケージをインストールしたい主なシチュエーションはコンテナ利用時

ファイルを見る

まずはファイル /usr/lib/.build-id/03/f4fad34e4c1a28712ec68f3c106fc200e25c60 を対象として少し調べてみましょう。

$ ls -l /usr/lib/.build-id/03/f4fad34e4c1a28712ec68f3c106fc200e25c60
lrwxrwxrwx. 1 root root 33 Sep  7 03:35 /usr/lib/.build-id/03/f4fad34e4c1a28712ec68f3c106fc200e25c60 -> ../../../../lib64/libnss_dns.so.2

→ ライブラリへのシンボリックリンクになっています

fileコマンドで見てみます。

$ file /lib64/libnss_dns.so.2
/lib64/libnss_dns.so.2: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=03f4fad34e4c1a28712ec68f3c106fc200e25c60, for GNU/Linux 3.2.0, stripped

→ "BuildID[sha1]=03f4fad34e4c1a28712ec68f3c106fc200e25c60" これがファイル名の 03/f4fad34e4c1a28712ec68f3c106fc200e25c60 に対応しています!

ライブラリに含まれている「Build ID」というものに対応していることがわかります。ディレクトリ名からも正しそうですね。

「Build ID」とは

Build IDは Fedora 8から導入された比較的古い機能で、FedoraやRHELでELF形式の実行ファイルや共有ライブラリ、カーネルモジュールに付与されているユニークなIDです。 FedoraやRHELではファイルの内容のSHA1ハッシュ値を使って生成しています。そのため内容が完全に同じであれば同一ですが、そうでなければ別のBuild IDが付与されます。

Fedora Wikiを見るとFedoraにBuild IDを導入した時の機能ページがあります。 fedoraproject.org

どのような課題を解決するための機能であるかという「The Problems」を見ると、debuginfoの素早い検証(Quick debuginfo validation)と、ダンプに対応するバイナリの発見(Finding binaries for dumps)の2項目が記載されています。

「debuginfo」 とは

RHELやFedoraでC言語などでのアプリケーションの開発・デバッグを行なっていない方には「debuginfo」は耳慣れない言葉かと思います。

debuginfo パッケージはrpmパッケージの一種です。C、C++言語などのソースレベルデバッグに使うデバッグ情報とソースコードを提供しています。FedoraとRHELのELFバイナリを含むrpmには、それぞれ対応するdebuginfoパッケージが提供されています(debugsource パッケージもありますがここではまとめて「debuginfoパッケージ」と呼びます。)。現在実行中のプログラムについてデバッグをしたい場合には、実行中のプログラムを含むrpmに対応するdebuginfoパッケージをインストールすることで、対応するソースコードを確認しながらデバッグができます。

実際にdebuginfoをインストールしてデバッグ作業につかうには、dnfのdebuginfo-installコマンドでもとのパッケージ名を指定してインストールします。

$ sudo dnf debuginfo-install glibc
(中略)
Dependencies resolved.
================================================================================================================
 Package                       Architecture       Version                   Repository                     Size
================================================================================================================
Installing:
 glibc-debuginfo               x86_64             2.35-17.fc36              updates-debuginfo             4.5 M
 glibc-debugsource             x86_64             2.35-17.fc36              updates-debuginfo             5.3 M

Transaction Summary
================================================================================================================
Install  2 Packages

Total download size: 9.8 M
Installed size: 54 M
Is this ok [y/N]: 

debuginfoパッケージがデバッグに利用するものだとわかりました。

debuginfoとBuild IDの関係

debuginfoパッケージとBuild IDとの関係をみていきます。

C言語等で典型的にはソースコードから、コンパイル、リンクという処理を経てELFファイル(実行ファイルや共有ライブラリなど)を作成します。 機械語とソースコードの対応関係や、メモリ上のデータと変数の対応関係などがデバッグ情報として機械語とともに生成され、ELFファイルに格納されます。 Build IDはリンク時に生成します。 FedoraやRHELでは、その後実行に必要な部分だけを含むELFファイルと、デバッグ時に必要な情報だけを含むdebuginfoパッケージ用のELFファイルを生成します。

リンク時にBuild IDが生成される

debuginfoパッケージを別のパッケージに分離することは技術上必須の作業ではありません。しかしサイズがかなり大きい(たとえば glibc パッケージは1.9MBに対して glibc-debuginfoとglibc-debugsource をあわせると9.8MB)ため、分離して配布されています。

「debuginfoの素早い検証」

実行に使っているrpmパッケージとdebuginfoパッケージのバージョンがずれてしまうことがあります。異なる内容のdebuginfoは間違った地図のようなもので、デバッグの役に立ちません。ここでBuild IDが活躍します。 実行ファイルのBuild IDと、debuginfoパッケージのELFファイルのBuild IDを対照して同じである場合にだけ利用します。 rpmパッケージのバージョンが対応することを利用しても確認できますが、この仕組みの方が簡単に行えます。(rpmのバージョン比較ではうまく処理できないケースにも対応できます。)

「ダンプに対応するバイナリの発見」

linuxはアプリケーションの異常時に「coredump」とよばれるプログラムのメモリの状態を出力します。このときに、利用している各ELFファイルのBuild IDを保存することで、新旧ふくめたどのELFファイルであるかを発見できるようになるというものです。Build ID登場以前には、ファイル名しか保存されていなかったため調査が難しくなることがありました。 /usr/lib/.build-id はこの Build IDからELFファイルへの逆引きのために利用されています。

以下はFedora 36で sleep コマンドへSIGQUITを送って出力させた coredump の情報です(注: systemd-coredumpによる情報収集によりcoreに含まれない情報も表示されています)。 build-id という欄で /usr/bin/sleep のBuild IDがわかり、"Module xxx with build-id xxxxxxxxxxxxx" のような行でライブラリのBuild IDもわかります。

           PID: 36828 (sleep)
           UID: 11956 (kmoriwak)
           GID: 11956 (kmoriwak)
        Signal: 3 (QUIT)
     Timestamp: Wed 2022-09-14 17:13:06 JST (14s ago)
  Command Line: sleep 4
    Executable: /usr/bin/sleep
 Control Group: /user.slice/user-11956.slice/user@11956.service/app.slice/app-org.gnome.Terminal.slice/vte-spawn-86bbb4b6-93e6-42cc-ae07-043f95a3625f.scope
          Unit: user@11956.service
     User Unit: vte-spawn-86bbb4b6-93e6-42cc-ae07-043f95a3625f.scope
         Slice: user-11956.slice
     Owner UID: 11956 (kmoriwak)
       Boot ID: 91c9b907c35f4893bb93d164afcaca94
    Machine ID: b6024ba9744641fdb43d59bec9a81a4d
      Hostname: kmoriwak.usersys.redhat.com
       Storage: /var/lib/systemd/coredump/core.sleep.11956.91c9b907c35f4893bb93d164afcaca94.36828.1663143186000000.zst (present)
     Disk Size: 20.5K
       Package: coreutils/9.0-8.fc36
      build-id: d5198b0c63efcb64e548b22d75f0349f8cac8e30
       Message: Process 36828 (sleep) of user 11956 dumped core.
                
                Module linux-vdso.so.1 with build-id ef533bb262f1a045f813981c3d60075160f0756a
                Module ld-linux-x86-64.so.2 with build-id 1eacb7c50f7ed20ef1fefda3aa9c67377686acf5
                Module libc.so.6 with build-id 9c5863396a11aab52ae8918ae01a362cefa855fe
                Module sleep with build-id d5198b0c63efcb64e548b22d75f0349f8cac8e30
                Metadata for module sleep owned by FDO found: {
(以下略)

複数バージョンのdebuginfoをシステムへ導入する

数年前(Fedora 26, RHEL 7)まで、複数バージョンのdebuginfoをシステムに同時にインストールすることはできませんでした。 実際にはシステムで同じパッケージの複数バージョンが動作しますので、これにあわせて複数バージョンのdebuginfoをインストールできるように 仕組みが改善されました。冒頭の /usr/lib/.build-id は、このタイミングで導入されています。

fedoraproject.org

同じソフトウェアの複数バージョンが動作するのはどんな時でしょう。

  • コンテナを利用している
  • i686とx86_64 のような複数アーキテクチャのプログラムが同時に利用されている
  • パッケージを更新したが古い実行ファイルで起動したプロセスの実行が続いている

さまざまなケースがありますが、OCIコンテナが普及して、コンテナイメージとホストOSの更新タイミングが一致しないケースが増えています。 そのような場合でもデバッグが容易になるよう、複数バージョンのdebuginfoをインストールできるようにする必要がありました。

/usr/lib/.build-id の通常rpmへの移動

実行用のrpmが/usr/lib/.build-idを提供するようになった

以前のdebuginfoパッケージには、対応する実行用のrpmパッケージ内の実行ファイルや共有ライブラリ等へのシンボリックリンクが /usr/lib/debug/.build-id/ 以下に 含まれていました。もしも複数バージョンのdebuginfoをインストールできるように制限だけをゆるくすると、実行ファイル等と実際には対応しないシンボリックリンクが多数作成され、coredumpに含まれるBuild IDからバイナリを発見することが難しくなります。

そのため、Build IDから実行ファイルを発見するためのシンボリックリンクを実行用のrpmパッケージが持つように変更されました。

Build IDの応用

Fedora 31以降やRHEL 8.5以降、RHEL 9.0以降では、この記事で登場したBuild IDを応用した debuginfod を利用できます。詳しくは下の記事をご覧ください。

rheb.hatenablog.com

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。