C++ユーザーの為のリンクの話２

[前回][1]に引き続き次を目標とします。なお環境は Linux で GCC (`g++`) と GNU binutils での話をします。

- リンク時の undefined reference エラーとは何なのかを理解する（[前回][1]）
- 実行時の cannot open libXXX.so エラーとは何なのかを理解する（この記事）

[1]: https://qiita.com/termoshtt/items/ffcd3dbc3928fc80caa1

オブジェクト (`*.o`) → 共有ライブラリ (`*.so`)
-------------------------------------------

[前回][1]のステップの続きで次は共有ライブラリを作りましょう。共有ライブラリを作成するには `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` を作りましょう

```cpp: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` の様なファイルを作り、

```:cuda.conf
/opt/cuda/lib64
/opt/cuda/nvvm/lib64
/opt/cuda/extras/CUPTI/lib64
```

共有ライブラリが置いてあるディレクトリを列挙します。これによって `LD_LIBRARY_PATH` を用いて一時的に追加するのと違って、永続的にいつでも共有ライブラリの場所を引けることになります。