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

第30回 共有ライブラリと依存関係[2]

この記事を読むのに必要な時間:およそ 4.5 分

前回共有ライブラリを使うバイナリファイルには,必要な共有ライブラリ名がライブラリのメジャーバージョン番号まで含めて記録されており,readelf -dlddコマンドでそれらの情報を知ることができることを紹介しました。

また,共有ライブラリのメジャーバージョンが更新された場合,そのライブラリを参照しているバイナリファイルも合わせて更新する必要があることも指摘しました。

その際触れたように,"readelf -d"や"ldd"コマンドを使えば「あるバイナリが参照しているライブラリ」は調べられるものの,⁠あるライブラリを参照しているバイナリ」を直接調べるツールは存在しません。

そのためPlamo Linuxでは,システム上に存在するELF形式のバイナリファイルについて,参照する共有ライブラリを事前に調べてデータベース化しておき,そのデータベースから「あるライブラリを参照しているバイナリ」を検索するようなツールを用意しています。それが今回紹介するget_depends.pyquery_depends.pyというスクリプトです。

get_depends.pyによる事前情報収集

前回も紹介したように,これらのスクリプトは筆者のGitHubのページからダウンロードできます。"get_depends.py"が必要な共有ライブラリを調べてデータベースに記録するスクリプト,"query_depends.py"がデータベースを参照するためのスクリプトです。いずれもGitHubのページの"RAW"リンクのURLからダウンロードし,"chmod +x"で実行可能にしておいてください。

get_depends.pyはファイルがELF形式かどうかをチェックするのにpython_magicというモジュールを使うため,あらかじめこのモジュールをインストールしておく必要があります。python_magicは,PyPI(Python Package Index)に登録されているので,pipコマンドでインストールできます。その際,pipコマンドはroot権限で実行し,python_magicモジュールを全てのユーザから利用できるようにしておいてください。

$ sudo pip install python_magic
[sudo] kojima のパスワード: XXXXX
Collecting python_magic
  Downloading python_magic-0.4.22-py2.py3-none-any.whl (12 kB)
Installing collected packages: python-magic
Successfully installed python-magic-0.4.22

python_magicのディレクトリが/usr/lib/python3.9/site-packages/以下に作成されていれば,正しくインストールできています。

$ ls /usr/lib/python3.9/site-packages/*magic*
/usr/lib/python3.9/site-packages/magic:
__init__.py  __pycache__/  compat.py  loader.py

/usr/lib/python3.9/site-packages/python_magic-0.4.22.dist-info:
INSTALLER  LICENSE  METADATA  RECORD  REQUESTED  WHEEL  top_level.txt

なお,sudoせずに一般ユーザのままでも"pip install python_magic"は実行できるものの,この場合,そのユーザのホームディレクトリ以下にモジュールがインストールされるため,sudoすると使えない,という事態になって首を捻ることになります(経験談⁠⁠。

python_magicは,ファイルの種類を判別するfileコマンド用に開発されたlibmagicライブラリをPythonから使うためのモジュールで,libmagicライブラリはファイルの先頭に書き込まれた識別情報やファイルの内容を元に,そのファイルの種類を「魔法のように」判別することから名付けられたライブラリです。

python_magicモジュールをインストールすれば,あとはget_depends.pyを実行するだけです。実行ファイルの中には一般ユーザからは読めないようなパーミッション設定になっているものもあるので,get_depends.pyはsudo経由で実行する必要があります。

$ sudo ./get_depends.py 
searching /bin
/bin/bash:ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=68f876940891eb6a20d3c34e82ae2685439a0ef7, stripped
/bin/bash is ELF
/bin/btrfs:ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-x86-64.so.2, BuildID[sha1]=c9d66839d500ce2ba0ecfe0c7d4fdf1b0c4bae97, for GNU/Linux 3.2.0, stripped
/bin/btrfs is ELF
....
zlib.cpython-3.8.so, /opt/libreoffice7.1/program/python-core-3.8.4/lib/lib-dynload/zlib.cpython-3.8.so, /lib64/ld-linux-x86-64.so.2, /lib64/ld-linux-x86-64.so.2

get_depends.pyが出力する膨大な表示を眺めると何をやっているか見当がつくと思いますが,このコマンドは,/binや/lib,/usrといった実行ファイルのありそうなディレクトリ以下の全ファイルを調べ,ELF形式のバイナリファイルだったらlddコマンドを適用して必要となるライブラリをチェックし,その結果を同じディレクトリのdepends.sql3というSqlite3形式のデータベースファイルに記録していきます。

作成されるdepends.sql3には,一行ごとに実行ファイル名とそのファイルのパス名必要な共有ライブラリ名とそのパス名が記録されています。

$ sqlite3 depends.sql3 
SQLite version 3.33.0 2020-08-14 13:23:32
Enter ".help" for usage hints.
sqlite> .schema
CREATE TABLE depends
       (base text, path text, soname text, realname text);
sqlite> select * from depends;
bash|/bin/bash|linux-vdso.so.1 (0x00007ffefddf3000)|
bash|/bin/bash|libreadline.so.7|/lib/libreadline.so.7
bash|/bin/bash|libdl.so.2|/lib/libdl.so.2
bash|/bin/bash|libc.so.6|/lib/libc.so.6
bash|/bin/bash|libncursesw.so.6|/lib/libncursesw.so.6
bash|/bin/bash|/lib/ld-linux-x86-64.so.2|/lib/ld-linux-x86-64.so.2
btrfs|/bin/btrfs|linux-vdso.so.1 (0x00007fffd5889000)|
btrfs|/bin/btrfs|libuuid.so.1|/lib/libuuid.so.1
....

query_depends.pyによる依存関係の検索

get_depends.pyで作成されたデータベースファイルを検索するコマンドがquery_depends.pyです。query_depends.pyには,簡単なヘルプメッセージを用意しています。

$ ./query_depends.py -h
Usage:
 ./query_depends.py [-b name] [-p path ] [-s soname ] [-r realname]
   ./depends.sql3 データベースを用いて,ライブラリの依存関係を調べる。
   -b name: name が含まれるELF形式のバイナリファイルが使う共有ライブラリを表示する
      -b cat とすれば /bin/cat だけでなく,bdftruncate や fc-cat もマッチする
      -b の場合,パス名は見ずに,ファイル名のみで検索する
   -p name: 検索の際にパス名も含めてマッチさせる。-p /bin/cat とすれば /bin/cat のみにマッチする
   -s soname: 共有ライブラリ soname を利用するバイナリファイルを表示する
      -s libgtk libgtk-3.so.0 や libgtk-x11-2.0.so もマッチする
      -s の場合,パス名は見ずに,共有ライブラリ名のみで検索する
   -r realname: 検索の際にライブラリのパス名も含める

先に見たように,depends.sql3は各行が(base text, path text, soname text, realname text)となっており,それに合わせてquery_depends.pyでも,-bオプションが"base",-pが"path",-sが"soname",-rが"realname" をそれぞれ対象に,文字列の部分一致で検索するという形にしています。

"-b"と"-p"はバイナリが参照している共有ライブラリを調べる,いわゆる「順引き」の機能なので省略し,今回は共有ライブラリを参照しているバイナリを調べる「逆引き」機能を試してみましょう。

"-s"はデータベースの"soname",すなわち共有ライブラリ名を元に検索するオプションで,たとえばFFmpegプロジェクトが開発している動画や音声の各種CODECSを操作するためのlibavcodecを使っているバイナリファイルを調べると,以下のようになりました。

$ ./query_depends.py -s libavcodec | cat -n
     1  libavcodec.so.57 used by these binaries
     2    ffmpeg3.4(/usr/bin/ffmpeg3.4)
     3    ffplay3.4(/usr/bin/ffplay3.4)
     4    ffprobe3.4(/usr/bin/ffprobe3.4)
     5    ffserver3.4(/usr/bin/ffserver3.4)
     6    libavdevice.so.57.10.100(/usr/lib/libavdevice.so.57.10.100)
     7    libavfilter.so.6.107.100(/usr/lib/libavfilter.so.6.107.100)
     8    libavformat.so.57.83.100(/usr/lib/libavformat.so.57.83.100)
     9  libavcodec.so.58 used by these binaries
    10    ffmpeg(/usr/bin/ffmpeg)
    11    ffplay(/usr/bin/ffplay)
    12    ffprobe(/usr/bin/ffprobe)
    13    mencoder(/usr/bin/mencoder)
    14    mplayer(/usr/bin/mplayer)
    15    mpv(/usr/bin/mpv)
    16    libavdevice.so.58.10.100(/usr/lib/libavdevice.so.58.10.100)
    17    libavfilter.so.7.85.100(/usr/lib/libavfilter.so.7.85.100)
    18    libavformat.so.58.45.100(/usr/lib/libavformat.so.58.45.100)
    19    libmpv.so.1.109.0(/usr/lib/libmpv.so.1.109.0)
    ....
    30    libvdpau_avcodec_plugin.so(/usr/lib/vlc/plugins/vdpau/libvdpau_avcodec_plugin.so)

libavcodecはFFmpeg-3.xの系列と4.xの系列でライブラリのメジャーバージョンが異なり,現在,開発が進行している4.x系列ではlibavcodec.so.58なのに対し,開発が終了した3.x系列ではlibavcodec.so.57となっています。

Plamo-7.3では4.x系のFFmpeg-4.3.2を採用しているものの,後方互換性を考慮して3.x系のFFmpeg-3.4のライブラリも残しています。そのため,"libavcodec"を検索すると,"libavcodec.so.57"を参照しているバイナリと"libavcodec.so.58"を参照しているバイナリの双方が表示されています。

一方,結果を詳しく見ると,"libavcodec.so.57"を参照しているのは,"ffmpeg3.4"や"libavformat.so.57.83.100"など,FFmpeg-3.4由来のバイナリに限られ,それ以外のバイナリはFFmpeg-4.3の"libavcodec.so.58"を参照しているようです。

すなわち,"libavcodec.so.57"を参照しているのは,この共有ライブラリと同じパッケージのバイナリファイルのみに限られるようなので,そろそろFFmpeg-3.4を削除しても問題は無さそうだ,と判断できます。

ELF形式のバイナリファイルは作成(リンク)時に存在している共有ライブラリを参照するので,以前はlibavcodec.so.57を参照していたバイナリでも,libavcodec.so.58がある環境でビルドし直せば,58の方を参照するようになります。

一方,さまざまな画像フォーマットを操作できるImageMagickでは,7.xのシリーズが開発の中心になっているものの,古い6.xのシリーズにしか対応していないソフトウェアも残っているためそちらもメンテナンスされており,Plamo-7.3でも両系統を収録しています。

このパッケージが提供するlibMagickWand共有ライブラリの使用状況は,このような結果になりました。

$ ./query_depends.py -s libMagicKWand | cat -n
     1        libMagickWand-6.Q16HDRI.so.6 used by these binaries
     2          emacs-26.2(/usr/bin/emacs-26.2)
     3          inkscape(/usr/bin/inkscape)
     4          inkview(/usr/bin/inkview)
     5          libMagick++-6.Q16HDRI.so.8.0.0(/usr/lib/libMagick++-6.Q16HDRI.so.8.0.0)
     6          libinkscape_base.so(/usr/lib/inkscape/libinkscape_base.so)
     ...
     9        libMagickWand-7.Q16HDRI.so.9 used by these binaries
    10          magick(/usr/bin/magick)
    11          libMagick++-7.Q16HDRI.so.5.0.0(/usr/lib/libMagick++-7.Q16HDRI.so.5.0.0)

この結果を見ると,emacsinkscapeという大物が6.x系のライブラリを要求しているので,まだしばらくは6.xと7.x双方のパッケージが必要そうなことがわかります。一方,emacsやinkscapeを更新する際には,まずImageMagick7対応がどうなっているのかを確認して,対応しているようならばそちらを参照するようにビルドすべきでしょう。

手元では,多くのバイナリから参照されている共有ライブラリを更新する際など,このようにライブラリの依存関係を確認しながら,パッケージの更新順序を検討しています。


get_depends.pyは実行した時点で存在するバイナリファイルを調べることしかできないため,get_pkginfo等でパッケージの更新を続けるとデータベースの情報と実際の環境とにずれが生じることになります。

そのような場合,一度depends.sql3を削除して,再度get_depends.pyでデータベースを作り直す必要があります。このあたり,改良の余地がありそうにも思うものの,データベースの再構築にはさほど時間はかからないし,依存関係が大きく変わるようなパッケージのupdateもそう頻繁にあるわけではないので,実用的には問題ないか,と考えています。

次回は,この依存情報データベースの面白い使い方を紹介しましょう。

著者プロフィール

こじまみつひろ

Plamo Linuxとりまとめ役。もともとは人類学的にハッカー文化を研究しようとしていたものの,いつの間にかミイラ取りがミイラになってOSSの世界にどっぷりと漬かってしまいました。最近は田舎に隠棲して半農半自営な生活をしながらソフトウェアと戯れています。

URLhttp://www.linet.gr.jp/~kojima/Plamo/index.html