続・玩式草子 ―戯れせんとや生まれけん―

第29回共有ライブラリと依存関係[1]

年度末や年度始めをバタバタと過しているうちに4月も下旬となってしまいました。日中は上着が無くても汗ばむくらいの陽気なのに、朝晩は冷えこむことも多く、着ていく服に悩む時期です。昨今は新型コロナではなくても、咳や発熱があれば社会生活が大幅に制限されるので、体調管理が大変です。

さて、最近はあまり取りあげる機会がなかったものの、筆者がまとめ役をやっているPlamo Linuxの開発は続いており、現在はPlamo-7.3のRC版をいくつか出して、正式版への最終調整といった段階に入っています。この段階では大規模な変更はほぼ終了していて、ライブラリの依存関係の確認や調整が主な作業になります。

Linuxをはじめ、最近のOSで「ライブラリ」と言うと、共有ライブラリ(shared library)が標準になり、さまざまな機能を提供するライブラリと、その機能を使うアプリケーションは、それぞれ独立に開発できるようになりました。

共有ライブラリは、1つのライブラリファイルを複数のアプリケーション(バイナリファイル)から共有することで、バイナリファイルのサイズを大幅に削減すると共に、ライブラリ側を修正するだけで、そのライブラリを使っている全てのバイナリに修正が適用できるなど、極めて便利な仕組みな一方、バージョンのミスマッチ等が生じるとシステム全体に影響が及ぶため、バイナリが期待しているライブラリのバージョンと実際のライブラリのバージョンには常に注意を払う必要があります。今回はこの共有ライブラリの仕組みや確認方法を紹介しましょう。

共有ライブラリの調べ方

CやC++で書かれたソースコードは、gccやg++等のコンパイラでインクルードファイル等の前処理や構文解析、最適化処理等を経てアセンブラ形式に変換され、gas等のアセンブラ機械語形式のオブジェクトファイルに変換されます。この段階では、同じソースコード内で定義された変数や関数はメモリ上のどこに配置するか決定できるものの、ライブラリに代表される外部の関数や変数への参照は未確定のままになっていて、それを解決するのはリンカ(ld)の仕事となります。

リンカは、ライブラリを含む複数のオブジェクトファイルを読み込んで、それぞれが参照、提供しているシンボル(変数や関数の名前)をチェックし、それらのメモリ上の配置を決定して実行可能なバイナリファイルを作ります。この際、共有ライブラリを使う動的リンク(dynamic link)と静的ライブラリを使う静的リンク(static link)では動作が異なり、静的リンクの場合はライブラリとして提供されているファイルを組み込んで必要なシンボルが全て揃ったバイナリファイルを作るのに対し、共有ライブラリを使う動的リンクの場合、作成されるバイナリファイルには参照すべき共有ライブラリ名のみが記され、共有ライブラリに含まれるシンボルの解決(メモリ上の位置の確定)は実行時のローダに委ねられます。

バイナリファイルに記載された共有ライブラリ名を調べるにはreadelf -dコマンドを使います。たとえば、"grep"コマンドの場合、Cの標準ライブラリ(libc.so.6)以外に、Perl風の正規表現を扱うためのlibpcre.so.1を必要としています。

$ readelf -d /usr/bin/grep

Dynamic section at offset 0x24e10 contains 25 entries:
 タグ        タイプ                       名前/値
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libpcre.so.1]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libc.so.6]
 0x000000000000000c (INIT)               0x402540
 0x000000000000000d (FINI)               0x41c69c

同様の情報はlddコマンドでも確認することができます。

$ ldd /usr/bin/grep 
linux-vdso.so.1 (0x00007ffedbfa4000)
libpcre.so.1 => /usr/lib/libpcre.so.1 (0x00007fa28a33b000)
libc.so.6 => /lib/libc.so.6 (0x00007fa28a155000)
/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fa28a5a0000)

両者の違いは、"readelf -d"がELF形式のバイナリファイルを調べてそこに記録されている共有ライブラリを表示するのに対し、"ldd"は指定したバイナリファイルが動作可能になるように必要な共有ライブラリを全て読み込んでみる、という点です。

"grep"の場合、"readelf -d"と"ldd"の結果でそれほど大きな違いはないものの、必要とする共有ライブラリがさらに別の共有ライブラリを要求するような場合、両者の結果は大きく異なってきます。

たとえば、Mate環境用のウィンドウ・マネージャmarcoの場合、"readelf -d"で表示されるのは3つの共有ライブラリだけなものの、"ldd"では82もの共有ライブラリが必要になると表示されました。

$ readelf -d  /usr/bin/marco 
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libmarco-private.so.2]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libglib-2.0.so.0]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libc.so.6]
 0x000000000000000c (INIT)               0x402000
 0x000000000000000d (FINI)               0x4034b4
 ...

$ ldd /usr/bin/marco | cat -n
   1          linux-vdso.so.1 (0x00007ffc0274c000)
   2          libmarco-private.so.2 => /usr/lib/libmarco-private.so.2 (0x00007fd6794fa000)
   3          libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0 (0x00007fd6793ce000)
   4          libc.so.6 => /lib/libc.so.6 (0x00007fd6791e8000)
   5          libcanberra-gtk3.so.0 => /usr/lib/libcanberra-gtk3.so.0 (0x00007fd678fe3000)
   6          libcanberra.so.0 => /usr/lib/libcanberra.so.0 (0x00007fd678dd3000)
   ...
  80          libmvec.so.1 => /lib/libmvec.so.1 (0x00007fd674a51000)
  81          libelogind.so.0 => /lib/libelogind.so.0 (0x00007fd674941000)
  82          libcap.so.2 => /lib/libcap.so.2 (0x00007fd674939000)

この場合、marcoが直接要求するのは"libmarco-private.so.2"と"libglib-2.0.so.0"なものの、"libmarco-private.so.2"は"libcanberra-gtk3.so.0"を必要とし、"libcanberra-gtk3.so.0"はGTK3用のさまざまな共有ライブラリを要求し…… という形で必要な共有ライブラリが芋づる式に増加しています。これら必要なライブラリのどれか1つでも欠けていれば、そのバイナリは動かなくなります。

共有ライブラリのバージョン

もう1つ、共有ライブラリで注意しなければならないのはバージョン番号です。先に見たgrepの例では"libpcre.so.1"が必要と表示されました。この場合、libpcre.so.2でも、libpcre.so.0でもなく、libpcre.so.1が必要です。

/usr/lib/libpcre.so.1を調べると、このファイルはシンボリックリンクになっていて、その実体はlibpcre.so.1.2.11になっています。

$ ls -l /usr/lib/libpcre.*
lrwxrwxrwx 1 root root      17  1月  6日  12:28 /usr/lib/libpcre.so -> libpcre.so.1.2.11*
lrwxrwxrwx 1 root root      17  1月  6日  12:28 /usr/lib/libpcre.so.1 -> libpcre.so.1.2.11*
-rwxr-xr-x 1 root root 283,848  7月  2日 2019年 /usr/lib/libpcre.so.1.2.11*

"libcre.so.1.2.11"という名前は、⁠pcre(PERL Compatible Regular Expression)機能を提供するライブラリ」「so(共有ライブラリ)版」で、メジャーバージョンが「1⁠⁠、マイナーバージョンが「2⁠⁠、リリースバージョンが「11」であることを示します。

1つの会社がライブラリからアプリケーションまで管理している商用ソフトウェアとは異なり、FOSSの世界ではライブラリとそれを使うアプリケーションは世界中で独立に開発されていてバージョンアップのタイミングもバラバラなため、アプリケーションを作っているうちに参照しているライブラリのバージョンが上がっていた、ということも普通に生じます。

「ライブラリのバージョンが上がると参照しているバイナリも全て作り直さないといけない⁠⁠、というのでは共有ライブラリのメリットが無くなってしまうので、共有ライブラリでは「ABI(Application Binary Interface)に互換性がある間は同じメジャーバージョンを維持するという決まりになっていて、上記の例ではバージョンアップで"libpcre.so.1.2.15"になったり、"libpcre.so.1.3.1"になっても、"libpcre.so.1"というメジャー番号が維持されている間は、このライブラリを参照しているバイナリを作り直す必要はありません。

共有ライブラリのバージョン番号付けのルールでは、バグやセキュリティホールの修正など軽微な修正はリリースバージョンで、新機能の追加などを伴なうものの、既存の機能に変更が無い場合はマイナーバージョンで、既存の機能を変更する必要がある場合はメジャーバージョンで、それぞれのバージョンアップを区別する、ということになってはいるものの、どの程度の変更で共有ライブラリのメジャーバージョンを更新するかはそれぞれの開発者に任されており、同じバージョン番号を長く維持するプロジェクトもあれば、かなり頻繁にバージョン番号を更新するプロジェクトもあります。

先に見たように、ELF形式のバイナリファイルには、必要なライブラリ名がメジャーバージョンまで含めて記録されているため、ライブラリのメジャーバージョンが更新されると、そのライブラリを使っているバイナリファイルを全て更新する必要があります。

そのため、ライブラリを提供するソフトウェアを更新する場合、まず一度ビルドしてみて作成される共有ライブラリのメジャーバージョンを調べ、以前のバージョンと互換性が保たれているかを確認する必要があります。

互換性があれば(=共有ライブラリのメジャーバージョンが同じなら)そのままパッケージ化して問題ないものの、メジャーバージョンが上がっていれば、そのライブラリを参照しているバイナリファイルを調べ、ライブラリ更新の影響がどこまで波及するかを確認しなければなりません。影響が広範囲に及ぶライブラリの場合、ライブラリを更新することで得られるメリット(新機能など)とそのライブラリを参照しているバイナリを作り直す手間をはかりにかけて、更新を先送りにすることもあります。

さらっと書いたものの「あるライブラリを参照しているバイナリファイル」を全て調べるのはかなり大変な作業です。というのも、先に触れた"readelf -d"や"ldd"コマンドのように、⁠バイナリファイルが参照しているライブラリ」を調べるツールはあるものの、⁠ライブラリを参照しているバイナリファイル」を直接調べるツールは存在しないからです。

そのためPlamo Linuxでは、それぞれのバイナリファイルが必要とする共有ライブラリをあらかじめ調べてデータベースに登録しておき、そのデータを使って「ライブラリを参照しているバイナリファイル」を調べるようなツールを用意しています。

さて、そのツールは……と言いたいところですが、ずいぶん長くなってしまったのでツールの紹介は次回に回すことにしましょう。


連載マンガのように山場を引き伸ばす意図はない(苦笑)ので簡単に紹介しておくと、次回紹介予定のツールはget_depends.pyquery_depends.pyというPythonスクリプトで、筆者のGitHubのページで公開しています。

解説は次回行うものの、短いPythonスクリプトで、使い方等もコメントで記載しているので、興味ある人は眺めてみてください。

おすすめ記事

記事・ニュース一覧