Linux標準リンカにおける共有ライブラリの検索順序

概要

今回はLinuxの標準リンカであるld.so、ld-linux.soの共有ライブラリの検索順序についてまとめます。

最近LAMP環境をソースインストール縛りで構築し、自分で新たにインストールしたライブラリのパスを通すなどして、以前よりライブラリの存在を強く意識するようになりました。

そこで、ライブラリがどのようにしてリンカによって検索されるのか疑問が沸き、ld.soのmanページを読んでみたところ、LD_LIBRARY_PATHよりも優先される値があることが興味深かったので、その点について調査したことを当記事でまとめてみることにしました。

本題に入る前に、ライブラリやリンカといった基本的な用語の説明からしていきます。

ライブラリ

ライブラリとは、様々なプログラムから必要とされる機能をまとめた、プログラムの部品のことで、静的ライブラリと共有ライブラリに大別できます。

静的ライブラリ

静的ライブラリとは、プログラムのビルド時(作成時)にその実行ファイル内に組み込まれるライブラリのことです。Linuxにおいては.a拡張子を持ちます。

共有ライブラリ

一方共有ライブラリとは、プログラムの実行時にロードされ、複数の実行ファイル間で共有されます。Linuxにおいては.so拡張子を持ちます。

リンク

リンクとは、プログラム本体に、利用するライブラリを連結することを指します。

ライブラリに静的ライブラリと共有ライブラリの2種類が存在するように、リンクにもスタティックリンクとダイナミックリンクの2種類が存在します。

スタティックリンク

スタティックリンクとは、静的ライブラリをプログラム本体に連結することです。つまり、実行ファイル作成時に必要なライブラリを予め埋め込んでおくということです。

オブジェクトファイル(.o拡張子)はソースファイルと実行ファイルの中間ファイルのことです。コンパイルした後に作成され、リンクが行われて実行ファイルとなる前の状態のファイルです。一応CPUが理解できる機械語なのですが、まだ実行をすることはできません。

このオブジェクトファイルにスタティックリンクを行うことで、必要なライブラリが埋め込まれ、実行ファイルという完全な状態に生まれ変わります。

メリットとしては、実行ファイル作成時点でライブラリが埋め込まれているため、プログラム実行環境側で必要なライブラリを準備しておく必要がなく、自己完結できるという点が挙げられます。

デメリットとしては、以下のように複数の実行ファイルが同じライブラリをスタティックリンクしている場合、実行ファイルごとにライブラリがコピーされているため、その分ディスクを消費するという点が挙げられます。

ダイナミックリンク

ダイナミックリンクとは、実行ファイル作成時にライブラリを埋め込むことはせず、プログラム実行時に初めてライブラリを呼び出して連結することです。

メリットとしては、複数の実行ファイルで同じライブラリを共有するため、実行ファイルごとのサイズを小さくできるという点が挙げられます。また、ライブラリを新しいバージョンに置き換えるような場合、スタティックリンクだと再リンクが必要になりますが、ダイナミックリンクだとプログラム実行時に自動的にリンクしてくれます(適切なパスが指定されていれば)。

デメリットとしては、プログラム実行環境側で必要なライブラリを予め用意しておかないとプログラムが実行できないという点が挙げられます。

ld.soとld-linux.so

Linuxにおいて、プログラム実行時に共有ライブラリを検索&ロードするリンカは、ld.soあるいはld-linux.soです。

両者の主な違いは、扱う実行ファイルのフォーマットです。ld.soはa.outという昔使われていた伝統的なフォーマットを扱うのに対し、ld-linux.soはELF(Executable and Linkable Format)という現在主流のファイルフォーマットを扱います。

そのため、現在のLinuxにおける標準リンカはld-linux.soの方です。自分の手元のUbuntu 20.04では/lib64ディレクトリ配下に、ld-linux-x86-64.so.2という名前で存在しています。

また、以下のような書式でリンカ自体を直接実行することもできます。

/path/to/ld-linux.so.* [OPTIONS] [PROGRAM [ARGUMENTS]] 

必要な共有ライブラリの確認

実行ファイルが必要とする共有ライブラリはlddコマンドを使用することで調べることができます。

lddコマンドはLD_TRACE_LOADED_OBJECTS環境変数に1をセットして、先程のld-linux.soを実行します。するとld-linux.soはライブラリの依存関係を表示します。つまり下記コマンドがlddコマンドの内部で行われます。

$ env LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /usr/bin/bash
	linux-vdso.so.1 (0x00007ffffe7ba000)
	libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f64c6d9b000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f64c6d95000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f64c6ba3000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f64c6f08000)

これでも必要な共有ライブラリを確認することができますが、面倒なので通常はlddコマンドで調べます。

では実際に手元のUbuntu 20.04でlddコマンドを実行してみます。ここではbashコマンドが必要とするライブラリを表示させています(コマンドの絶対パスを指定)。

$ ldd /usr/bin/bash
	linux-vdso.so.1 (0x00007ffcd92e6000)
	libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007fbfce3cf000)
	libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fbfce3c9000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbfce1d7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fbfce53c000)

“=>”の左側が必要とされるライブラリ名(soname: lib + ライブラリ名 + .so + バージョン)で、右側が実際に利用されるライブラリの絶対パスです。

引数には実行プログラムだけでなく、共有ライブラリ名を指定することもできます。つまり、共有ライブラリが必要とする共有ライブラリを調べることができます。ここではlibtinfo.so.2が必要とする共有ライブラリを表示させています。

$ ldd /lib/x86_64-linux-gnu/libtinfo.so.6
	linux-vdso.so.1 (0x00007ffeeabce000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd0d8448000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd0d867b000)

共有ライブラリの検索順序

プログラム実行時に、リンカであるld-linux.soによって共有ファイルが検索&ロードされるわけですが、複数の検索場所があり、またそれらの検索場所に優先順位がつけられています。大まかな検索順序は以下の通りです。

  1. DT_RPATH(DT_RUNPATHが存在しない場合)
  2. LD_LIBRARY_PATH
  3. DT_RUNPATH
  4. /etc/ld.so.cache
  5. デフォルト(/usr、/lib/usr)

では上から順番に詳しく説明していきます。

① DT_RPATH

最も優先されるディレクトリはDT_RPATHの値です。この値はビルド時に実行ファイルの一部として保存される特殊な値です。

最も優先されるといっても、DT_RUNPATHが指定されていない時の場合です。DT_RUNPATHが指定されていれば、このDT_RPATHは無視され、その結果LD_LIBRARY_PATHが最も優先されるようになります。

リンクを行うldコマンドの-rpathオプションで、ディレクトリを指定することで、DT_RPATHとして実行ファイルにバイナリで埋め込まれます。といっても静的ライブラリのようにライブラリ本体がそのまま埋め込まれるのではなく、リンクする共有ライブラリのパス情報だけが埋め込まれます。

DT_RPATHの確認方法

このように実行ファイルにバイナリで埋め込まれるわけですが、objdumpコマンドreadelfコマンドでDT_RPATHの値を確認することができます。

まず、objdumpコマンドはオブジェクトファイルの情報を表示するコマンドです。実行ファイルの情報も表示することができ、-p(–private-headers)オプションをつけて実行します。

では実際にobjdumpコマンドを実行してみます。下記コマンドは自分で新たにソースインストールしたApacheのhttpコマンドのフォーマットについての情報を表示しています。

$ objdump -p /usr/local/httpd/bin/httpd

/usr/local/httpd/bin/httpd:     ファイル形式 elf64-x86-64

プログラムヘッダ:
    PHDR off    0x0000000000000040 vaddr 0x0000000000400040 paddr 0x0000000000400040 align 2**3
         filesz 0x00000000000001f8 memsz 0x00000000000001f8 flags r-x
  INTERP off    0x0000000000000238 vaddr 0x0000000000400238 paddr 0x0000000000400238 align 2**0
         filesz 0x000000000000001c memsz 0x000000000000001c flags r--
    LOAD off    0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
         filesz 0x0000000000098004 memsz 0x0000000000098004 flags r-x
    LOAD off    0x0000000000098d80 vaddr 0x0000000000698d80 paddr 0x0000000000698d80 align 2**21
         filesz 0x0000000000004294 memsz 0x0000000000007af0 flags rw-
・・・・・・・・

このようにたくさん出力されますが、注目していただきたいのは、次の動的セクション部分です。

・・・・・
動的セクション:
  NEEDED               libpcre.so.1
  NEEDED               libaprutil-1.so.0
  NEEDED               libexpat.so.1
  NEEDED               libapr-1.so.0
  NEEDED               librt.so.1
  NEEDED               libcrypt.so.1
  NEEDED               libpthread.so.0
  NEEDED               libdl.so.2
  NEEDED               libc.so.6
  RPATH                /usr/local/lib:/usr/local/httpd/lib <------ココ
  INIT                 0x000000000042aca0
  FINI                 0x000000000047414c
  INIT_ARRAY           0x0000000000698d80
  INIT_ARRAYSZ         0x0000000000000008
  FINI_ARRAY           0x0000000000698d88

NEEDEDというhttpdコマンドが必要とするライブラリが表示されるエントリの次に、RPATHというエントリがあります。ここに表示されているディレクトリがDT_RPATHに格納されている値です。

ここでは、/usr/local/libと/usr/local/httpd/libが指定されています。つまり、httpdコマンドの実行時にこれらのディレクトリが最も優先的にリンカによって検索され、見つけた共有ライブラリがロードされます。

readelfコマンドを使用する場合は-dオプションを使用して、実行ファイルを指定します。

$ readelf -d /usr/local/httpd/bin/httpd

Dynamic section at offset 0x98d98 contains 33 entries:
 タグ        タイプ                       名前/値
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libpcre.so.1]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libaprutil-1.so.0]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libexpat.so.1]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libapr-1.so.0]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [librt.so.1]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libcrypt.so.1]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libdl.so.2]
 0x0000000000000001 (NEEDED)             共有ライブラリ: [libc.so.6]
 0x000000000000000f (RPATH)              ライブラリの rpath: [/usr/local/lib:/usr/local/httpd/lib]
 0x000000000000000c (INIT)               0x42aca0
 0x000000000000000d (FINI)               0x47414c
 0x0000000000000019 (INIT_ARRAY)         0x698d80

こちらの方が出力も少なくシンプルなのでオススメです。

DT_RPATHとDT_RUNPATHの関係

実はこのDT_RPATHは、manページによると非推奨になっています。なぜならば、最も検索優先度の高いDT_RPATHがビルド時に実行ファイルに埋め込まれるということは、ユーザが自由に設定できるLD_LIBRARY_PATHや/etc/ld.so.cacheで上書きすることができず、融通が利かなくなるからです。違う共有ライブラリを検索して欲しくなった場合は再ビルドするしかないです。

この問題を解消するために新しく導入されたのがDT_RUNPATHです。後のDT_RUNPATHでも説明しますが、この値がセットされていると、DT_RPATHは無視されます。

そうなると最も優先される検索場所はLD_LIBRARY_PATHになります。

② LD_LIBRARY_PATH

LD_LIBRARY_PATH環境変数は以下のようにして設定します。

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/library

注意しなければならないのは、下記リンクにあるように$HOME/.profile、/etc/profile、/etc/environmentでLD_LIBRARY_PATHを設定しても反映されないということです。

EnvironmentVariables - Community Help Wiki

特定ユーザで永続的に設定したい場合は、~/.bashrc等に記述し、システム全体で永続的に検索して欲しい場合は、後述の/etc/ld.so.confで設定します。

③ DT_RUNPATH

上記のようなDT_RPATHの問題とDT_RUNPATHが生まれた背景は以下の記事が参考になります。

https://software.intel.com/sites/default/files/m/a/1/e/dsohowto.pdf

RpathIssue - Debian Wiki

DT_RUNPATHをセットするには、ldコマンドに–enable-new-dtagsオプションを指定します。すると、DT_RPATHもセットされるのですが、両方あるということはDT_RPATHが無視されます。

man ldを実行し、–enable-new-dtagsオプションをみてみると、以下のような一文が記述されていました。

By default, the new dynamic tags are not created.

つまりldコマンドのデフォルトでは、DT_RUNPATHがセットされないため、必要なら–enable-new-dtagsを指定しなければいけないようです。

DT_RPATHが非推奨となっているのに、デフォルトでDT_RUNPATHがセットされないのはなぜなのか分かりません。

④ /etc/ld.so.cache

/etc/ld.so.cacheは、実行ファイルに必要な共有ライブラリの場所が記述されたバイナリのキャッシュファイルです。ld-linux.soに検索して欲しいディレクトリは、後述する/etc/ld.so.confや/etc/ld.so.conf.dディレクトリ内のファイルで設定しますが、これだと検索が面倒なので、/etc/ld.so.cacheという一つのファイルにバイナリ形式でまとめています。そうすることで高速な検索が可能になります。

バイナリなのでcatコマンド等で中身を閲覧することはできませんが、下記コマンドを実行することで中身を閲覧することができます。

$ ldconfig -p
450 個のライブラリがキャッシュ `/etc/ld.so.cache' 内で見つかりました
	p11-kit-trust.so (libc6,x86-64) => /lib64/p11-kit-trust.so
	libz.so.1 (libc6,x86-64) => /lib64/libz.so.1
	libyaml-0.so.2 (libc6,x86-64) => /lib64/libyaml-0.so.2
	libyajl.so.2 (libc6,x86-64) => /lib64/libyajl.so.2
	libxtables.so.10 (libc6,x86-64) => /lib64/libxtables.so.10
・・・・・

/etc/ld.so.conf

ld-linux.soはデフォルトでは/libや/usr/libを検索しますが、自分で新しくインストールしたライブラリにパスを通したいといった時は、/etc/ld.so.confにそのライブラリへのパスを記述します。

例として、共有ライブラリの検索に/usr/local/lib64を追加したい場合は以下のように設定します。

$ sudo vi /etc/ld.so.conf
/usr/local/lib64

しかし、実際/etc/ld.so.conf内は以下のような設定がされていることが多いと思います。

include ld.so.conf.d/*.conf

これは、/etc/ld.so.conf.dディレクトリにある.confファイルを取り込むという意味です。つまり、この/etc/ld.so.confに直接パスを書くよりかは、/etc/ld.so.conf.dディレクトリ配下にファイルを作成し、そこにパスを記述した方がスマートです。

以下の例では、/usr/local/lib64のパスだけを記述したusr-local-lib64.confファイルを作成し、ll(ls -l)コマンドで確認しています。

$ ll /etc/ld.so.conf.d
合計 24
-rw-r--r--. 1 root root 26  8月  8  2019 bind-export-x86_64.conf
-rw-r--r--  1 root root 19  8月  9  2019 dyninst-x86_64.conf
-r--r--r--. 1 root root 63  8月  8  2019 kernel-3.10.0-1062.el7.x86_64.conf
-r--r--r--  1 root root 63 12月 19 01:39 kernel-3.10.0-1160.11.1.el7.x86_64.conf
-rw-r--r--. 1 root root 17  8月  8  2019 mariadb-x86_64.conf
-rw-r--r--  1 root root 17  1月 29 15:43 usr-local-lib64.conf

ldconfigコマンド

以上のように新たに設定ファイルを作成・編集しましたが、これだけではld-linux.soに検索してもらえません。なぜならば、ld-linux.soが参照する/etc/ld.so.cacheファイルに変更が反映されていないからです。ではどうするかというと、ldconfigコマンドを実行して/etc/ld.so.cacheファイルを更新します。

ldconfigコマンドだけだと何も出力されないので、下記のように-vオプションを付けることをお勧めします。

$ ldconfig -v
・・・・・
/usr/local/lib64:
	libgcc_s.so.1 -> libgcc_s.so.1
	liblsan.so.0 -> liblsan.so.0.0.0
	libatomic.so.1 -> libatomic.so.1.2.0
	libitm.so.1 -> libitm.so.1.0.0
	libgomp.so.1 -> libgomp.so.1.0.0
	libasan.so.6 -> libasan.so.6.0.0
・・・・・

このように追加したいディレクトリが表示されていれば設定完了です。

⑤ デフォルトパス(/lib, /usr/lib)

上記全ての設定が特にされていない場合は、デフォルトパスである/libや/usr/libが検索対象になります。ここでも見つけられないと、エラーが発生してプログラムを実行することはできません。

/libや/usr/libとは何か具体的にわからなければ、以下の記事を参考にしてみてください。FHSという様々なディレクトリの役割を定めた仕様書を読んで自分なりにまとめた記事です。

FHS(Filesystem Hierarchy Standard) 3.0のまとめ

LD_DEBUGでパス検索確認

LD_DEBUG環境変数にlibsを指定してコマンドを実行すると、ld-linux.soが実際にどのディレクトリから共有ライブラリを探索したのか、細かく出力してくれます。

下記コマンドによって、bashコマンドに必要な共有ライブラリが/etc/ld.so.cacheから検索されていることが分かります。

$ env LD_DEBUG=libs bash
     90695:	find library=libtinfo.so.6 [0]; searching
     90695:	 search cache=/etc/ld.so.cache
     90695:	  trying file=/lib/x86_64-linux-gnu/libtinfo.so.6
     90695:
     90695:	find library=libdl.so.2 [0]; searching
     90695:	 search cache=/etc/ld.so.cache
     90695:	  trying file=/lib/x86_64-linux-gnu/libdl.so.2
     90695:
     90695:	find library=libc.so.6 [0]; searching
     90695:	 search cache=/etc/ld.so.cache
     90695:	  trying file=/lib/x86_64-linux-gnu/libc.so.6
     90695:
・・・・・・・

では先程登場した、DT_RPATHが設定されているhttpdコマンドではどうなるのか試してみます。DT_RPATHには/usr/local/libと/usr/local/httpd/libが指定されています。

$ env LD_DEBUG=libs /usr/local/httpd/bin/httpd -t
     15030:	find library=libpcre.so.1 [0]; searching
     15030:	 search path=/usr/local/lib/tls/x86_64:/usr/local/lib/tls:/usr/local/lib/x86_64:/usr/local/lib:/usr/local/httpd/lib/tls/x86_64:/usr/local/httpd/lib/tls:/usr/local/httpd/lib/x86_64:/usr/local/httpd/lib		(RPATH from file /usr/local/httpd/bin/httpd)
     15030:	  trying file=/usr/local/lib/tls/x86_64/libpcre.so.1
     15030:	  trying file=/usr/local/lib/tls/libpcre.so.1
     15030:	  trying file=/usr/local/lib/x86_64/libpcre.so.1
     15030:	  trying file=/usr/local/lib/libpcre.so.1
     15030:
     15030:	find library=libaprutil-1.so.0 [0]; searching
     15030:	 search path=/usr/local/lib:/usr/local/httpd/lib/tls/x86_64:/usr/local/httpd/lib/tls:/usr/local/httpd/lib/x86_64:/usr/local/httpd/lib		(RPATH from file /usr/local/httpd/bin/httpd)
     15030:	  trying file=/usr/local/lib/libaprutil-1.so.0
     15030:	  trying file=/usr/local/httpd/lib/tls/x86_64/libaprutil-1.so.0
     15030:	  trying file=/usr/local/httpd/lib/tls/libaprutil-1.so.0
     15030:	  trying file=/usr/local/httpd/lib/x86_64/libaprutil-1.so.0
     15030:	  trying file=/usr/local/httpd/lib/libaprutil-1.so.0

このように最も優先度が高いDT_RPATHが検索されています。

最後に

今回はLinuxの標準リンカがライブラリをどのように検索するのか、基礎用語の説明も含めてまとめてみました。

個人的に興味深かったのは、DT_RPATHとDT_RUNPATHの関係性です。DT_RUNPATHの登場で優先度の低いLD_LIBRARY_PATHや/etc/ld.so.cacheの設定を活かせるようになったわけで、とても興味深いなと思いました。

しかし非推奨であるはずのDT_RPATHが依然として存在するので、とてもややこしく感じますし、未だ理解ができずモヤモヤしているところがあります。

今後DT_RPATHのせいで自分に何か困ることが起きるのか分かりませんが、LD_LIBRARY_PATHが反映されないなと思ったらDT_RPATHを疑ってみることにします。

Comments

Copied title and URL