前回に引き続き次を目標とします。なお環境は Linux で GCC (g++
) と GNU binutils での話をします。
- リンク時の undefined reference エラーとは何なのかを理解する(前回)
- 実行時の cannot open libXXX.so エラーとは何なのかを理解する(この記事)
オブジェクト (*.o
) → 共有ライブラリ (*.so
)
前回のステップの続きで次は共有ライブラリを作りましょう。共有ライブラリを作成するには g++ -shared
を使います
$ g++ -shared a.o b.o -o libab.so
/usr/bin/ld: b.o: warning: relocation against `_ZSt4cout@@GLIBCXX_3.4' in read-only section `.text'
/usr/bin/ld: a.o: relocation R_X86_64_PC32 against symbol `_ZSt4cout@@GLIBCXX_3.4' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: bad value
-fPIC
(Position Independent Code) オプションをつけてソースコードを再コンパイルする必要があると怒られます。
$ g++ -fPIC -c a.cpp
$ g++ -fPIC -c b.cpp
$ g++ -shared a.o b.o -o libab.so
今度は無事出来上がりました。
$ nm -C libab.so | grep func
0000000000001139 T func_a()
00000000000011c9 T func_b()
func_a()
, func_b()
共に存在していることが分かります。
共有ライブラリをリンクする
さて問題はここからです。共有ライブラリ libab.so
を使ったプログラム main.cpp
をコンパイルして見ましょう
$ g++ main.o libab.so
$ ./a.out
./a.out: error while loading shared libraries: libab.so: cannot open shared object file: No such file or directory
はい libab.so が見つからないと言っていますね。でも今まさにリンクしてあげたはずです。一体何が起こったのでしょう?
まず nm
で a.out
を見てみましょう
$ nm -C a.out | grep func
U func_a()
func_a()
が Undefined になっていますね?これはオブジェクトファイルをそのまま使ったときやアーカイブを使ったときにはちゃんと含まれていたはずです。
このように共有ライブラリをリンクした場合には、実行ファイルには共有ライブラリの中身が含まれません。実行ファイルは共有ライブラリにある実装に処理を移すために、実行時に 共有ライブラリを探してロードしてその中にある実装に処理を任せます。実行ファイルがどの共有ライブラリを探すかは実行ファイルに書かれて、objdump
を使うと表示できます
$ objdump -x a.out
...
Dynamic Section:
NEEDED libab.so
NEEDED libstdc++.so.6
NEEDED libm.so.6
NEEDED libgcc_s.so.1
NEEDED libc.so.6
INIT 0x0000000000001000
FINI 0x00000000000011c8
INIT_ARRAY 0x0000000000003da8
INIT_ARRAYSZ 0x0000000000000008
FINI_ARRAY 0x0000000000003db0
FINI_ARRAYSZ 0x0000000000000008
GNU_HASH 0x0000000000000308
STRTAB 0x00000000000003d0
SYMTAB 0x0000000000000328
STRSZ 0x00000000000000b8
SYMENT 0x0000000000000018
DEBUG 0x0000000000000000
PLTGOT 0x0000000000004000
PLTRELSZ 0x0000000000000018
PLTREL 0x0000000000000007
JMPREL 0x0000000000000578
RELA 0x00000000000004b8
RELASZ 0x00000000000000c0
RELAENT 0x0000000000000018
FLAGS_1 0x0000000008000000
VERNEED 0x0000000000000498
VERNEEDNUM 0x0000000000000001
VERSYM 0x0000000000000488
RELACOUNT 0x0000000000000003
...
-x
(=--all-header
) は詳細情報を全て表示するオプションなので必要なところだけ抜き出しましたが、Dynamic Section と言うところに NEEDED libab.so
という表示があることが分かりますね。この様に実行ファイルには名前で必要な共有ライブラリが書かれているため、システムのどこかからそれを見つけてこないと行けません。
これを実現するのが動的リンカー ld-linux.so
であり、これが検索した結果を表示するコマンドが ldd
です
$ ldd a.out
linux-vdso.so.1 (0x00007fff32d8f000)
libab.so => not found
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f9b4b0cf000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f9b4af89000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f9b4af6f000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f9b4ada6000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f9b4b2f3000)
この =>
の左にあるのが a.out
の中に書いてある必要な共有ライブラリの名前、右が ldd
が見つけた共有ライブラリが実際にある場所です。いま libab.so
が not found になっていますね。これにより実行ファイルが実行出来なかったわけです。
でも今すぐ手元に libab.so はありますね? ld-linux.so
は共有ライブラリを探すパスを一時的に追加する機能があり、そのための環境変数が LD_LIBRARAY_PATH
です。つまり次の様にすると
%LD_LIBRARY_PATH=. ldd ./a.out
linux-vdso.so.1 (0x00007ffeb6ff2000)
libab.so => ./libab.so (0x00007fbdf145e000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007fbdf1241000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fbdf10fb000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007fbdf10e1000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fbdf0f18000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fbdf146a000)
$ LD_LIBRARY_PATH=. ./a.out
This is func_a!
となり無事に共有ライブラリをリンクした実行ファイルを実行することができました!!
ではリンク時に何をしているのか?
これを確認するために例えば存在しない関数 func_c()
を呼び出す別の main2.cpp
を作りましょう
void func_c();
int main() {
func_c();
return 0;
}
func_c()
なんてのは libab.so には入っていないので
$ g++ main2.cpp libab.so
/usr/bin/ld: /tmp/ccs8FHuO.o: in function `main':
main2.cpp:(.text+0x5): undefined reference to `func_c()'
となります。つまり名前の解決をして、処理を引き渡す関数が存在することを確認して、一旦 libab.so
とはお別れします。
共有ライブラリを探す手順
先ほど共有ライブラリを探すパスを追加する方法として LD_LIBRARY_PATH
環境変数を紹介しましたが、普段多くのプログラムが共有ライブラリを使用していますが、我々は毎回この環境変数を設定しているわけではありません。ではどうやって ld-linux.so
は共有ライブラリを探しているのでしょうか?
これを実現しているのが ldconfig
コマンドです。これは名前から共有ライブラリの場所を引くためのキャッシュを提供してくれます
$ ldconfig -p | grep libstdc++.so
libstdc++.so.6 (libc6,x86-64) => /usr/lib/libstdc++.so.6
libstdc++.so (libc6,x86-64) => /usr/lib/libstdc++.so
これに登録されているものは追加で LD_LIBRARY_PATH
を追加しなくても自動的に探してくれるようになります。実際上の例でも libab.so
以外の共有ライブラリの場所はここの情報を元に得られています。
これはキャッシュになっているので、実行ファイルが実行される度に対象となるディレクトリを走査してファイルを探すのではなく、一旦走査を行ってその結果を使います。例えば新しく共有ライブラリをシステムに追加して、それを ld-linux.so
が発見出きるようにするには、/etc/ld.so.conf
か /etc/ld.so.conf.d/xxx.conf
を作成して、ldconfig
コマンドを実行することで、ファイルが走査されてキャッシュが生成されます。典型的には /etc/ld.so.conf.d/cuda.conf
の様なファイルを作り、
/opt/cuda/lib64
/opt/cuda/nvvm/lib64
/opt/cuda/extras/CUPTI/lib64
共有ライブラリが置いてあるディレクトリを列挙します。これによって LD_LIBRARY_PATH
を用いて一時的に追加するのと違って、永続的にいつでも共有ライブラリの場所を引けることになります。