リンカのライブラリ探索 [Raspberry Pi]
macOS上にRaspbianのクロス環境を作って、XcodeのエディタなどのUIを使えるようにしたんだけど、外部のライブラリをリンクするときによくわからないことが起こっていた。仕事にも使ってるのでかなり困って悩んでた。
例えばlibxml2をリンクするとgcc(linaroのもの)だと問題ないのに、新しいclangを使うとうまくいかない。エラーの内容を見ると、
unixではライブラリの置き場所がだいたい決まっている。
ソフトウェアを書いて、ライブラリをリンクするときは必要なライブラリがどこにあるかを知らないといけないが、libcなんかの必ず必要になるものとか汎用のものは場所はほぼ決まっているようである。リンカはそれをどうやって知るか、というのを実は僕はわかってなかった。改めて勉強した。
何かRaspberry Pi上で動くコードを書いて、そのコードからOSのシステムコール以外を全く呼ばない、ということはまずない。何も呼んでいないようでも、例えばCで書けば暗黙のうちにlibcというランタイムライブラリがリンクされている。システムコールのCとのブリッジやstdio.hの中の関数はその中に実装されている。
僕がunixを使い出した80年代はものごとはすごく簡単で、ないものは自分で書くしかなかった。外部のライブラリとして使ったのはせいぜいXtやMotifといったX windowのTool Kitと本体のX11ぐらいだった。これは当時libxxxx.aという静的ライブラリで、コンパイルしてa.outを作るとそれに全部がコピーされた。X11は自分自身(とlibc)以外に依存するライブラリもなく、コンパイル後リンクが正常にできれば、あとから問題が起こるということはなかった。
そのうちシェアドライブラリlibxxxx.so(macOSだとlibxxxx.dylib)が考え出された。実行形式ファイルのサイズとメモリ圧迫の両方を軽減することができるアイデアである(厳密に言えば別々に解決するアイデアが合体したものである)。このために、実行形式のバイナリファイルはコードが呼んでいる関数などの名前を書いておくだけでコピーせず、ライブラリの方はメモリの位置に依存しないようなバイナリになっている。実行されるときにリンクローダは必要なライブラリの実行コードをメモリに配置して、関数の名前の表にその位置を書き込んでから、main()関数を呼び出す。
実行形式ファイルの中にはその名前の表と、どのライブラリに依存しているか、そのライブラリがファイルとしてどこにあるのか(パス)、がリンカによって書き込まれる。さらにライブラリがまた他のライブラリに依存している場合には、同じことが入れ子に書かれることになる。それがローダによって全て解決できて初めてコードが実行される。
いいアイデアなんだけど、非常に複雑になった。コンパイル時点と実行時でライブラリの場所が変わったりするとローダがわからない、しかしmain()関数を含んだ実行形式ファイルは別のシステム上で動作させることも考慮する必要があって、全てのシステムでライブラリの位置が同じになっているとは限らない。
さらに、シェアドライブラリの場合、それを呼び出すコードとは別にバージョンを設定できる。呼び出し形式が一致していればライブラリだけをバージョンアップすることができるが、微妙な互換性の問題も発生する。ある実行ファイルではあるバージョンのライブラリでないと動作しない、などということも起こり得る。
そういったもろもろの問題を一気に解決できるアイデアは今の所ないらしくて、ライブラリの指定の仕方を複数用意して、それぞれの間に優先順位をつけることで、柔軟性を持たせるということがされているようである(macOSではもう少し賢い方法(Framework)が取られているが、実際にはそれほど有効に機能していないように見える)。
具体的にいうと、おおまかには優先する順番で
従って、うまくいっていれば何の問題もないが、いったん問題が発生すると原因の特定が非常に難しくなる。
ここで、ほかとなんとなく違っている/etc/ld.so.confについて。なんか/etcなんてディレクトリにあるのがちょっと変だけど、構造が他のrcなんかと同じだ、ということなんだろう。/etc/ld.so.confの中身は
ということで、このどこかに必要なライブラリのパスがあればリンクは成功する。ところがクロス環境の場合、話がややこしくなる。パスは基本的に絶対パス指定されるようである。それが不便なのでsysrootのメカニズムが作られたんだけど、その大元のリンカローダのパス指定は悩ましい。オプションは絶対パス指定でそれはいいというかしょうがないんだけど、例えばライブラリがさらに他のライブラリを参照している場合、うまくいかなくなることがある。
セーフティネットとしての/etc/ld.so.confもクロス環境ではホストの/etcを参照してしまうので、機能しなくなる。sysrootの下に/etcを作っても当然機能しない。従ってクロス環境では環境変数で指定するかオプションでいちいち指定するしかなくなる。
ところがなぜか環境変数LD_LIBRARY_PATHに指定しても無視されてしまう(環境変数はライブラリの入れ子には影響しないということかもしれないけど、binutilsリンカのmanページを読んでもよくわからない)。sysrootメカニズムの思想の問題かもしれないんだけど、そこまで突っ込めていない。どのみち環境変数は効果が副作用的で、あまり好きではないので構わないといえばそうなんだけど。
結局オプションでいちいち指定するしか今のところ解決策がない。具体的にはリンカに直接
でもなんでライブラリに入れ子がない場合はなにもしなくてもリンクできるんだろう。それもよくわからない。これも専門家の人、教えて。
例えばlibxml2をリンクするとgcc(linaroのもの)だと問題ないのに、新しいclangを使うとうまくいかない。エラーの内容を見ると、
/usr/local/raspbian-sdk/prebuilt/bin/arm-linux-gnueabihf-ld: warning: libdl.so.2, needed by /usr/local/raspbian-sdk/prebuilt/bin/../../sysroot/usr/lib/arm-linux-gnueabihf/libxml2.so, not found (try using -rpath or -rpath-link) /usr/local/raspbian-sdk/prebuilt/bin/arm-linux-gnueabihf-ld: warning: libicui18n.so.57, needed by /usr/local/raspbian-sdk/prebuilt/bin/../../sysroot/usr/lib/arm-linux-gnueabihf/libxml2.so, not found (try using -rpath or -rpath-link) .....どうやらlibxml2が依存しているライブラリが見つからない、と言っているようである。でもちゃんとあるのになんでかなあ、と思っていた。これは僕が最近のLinuxのリンクのメカニズムをちゃんと理解していないせいだった。とりあえずエラーを出さずに済ます方法はわかったんだけど、複雑ですごく難しい....
unixではライブラリの置き場所がだいたい決まっている。
/lib システムが使うライブラリ /usr/lib ユーザが開発に使う追加ライブラリ /usr/local/lib ユーザが追加したライブラリなどがある。この分類はそれほど厳密なものではなくて、ディストリビューションなどによっても違っているし、macOSなんかに至っては「/lib」そのものがなくなってしまっている。
ソフトウェアを書いて、ライブラリをリンクするときは必要なライブラリがどこにあるかを知らないといけないが、libcなんかの必ず必要になるものとか汎用のものは場所はほぼ決まっているようである。リンカはそれをどうやって知るか、というのを実は僕はわかってなかった。改めて勉強した。
何かRaspberry Pi上で動くコードを書いて、そのコードからOSのシステムコール以外を全く呼ばない、ということはまずない。何も呼んでいないようでも、例えばCで書けば暗黙のうちにlibcというランタイムライブラリがリンクされている。システムコールのCとのブリッジやstdio.hの中の関数はその中に実装されている。
僕がunixを使い出した80年代はものごとはすごく簡単で、ないものは自分で書くしかなかった。外部のライブラリとして使ったのはせいぜいXtやMotifといったX windowのTool Kitと本体のX11ぐらいだった。これは当時libxxxx.aという静的ライブラリで、コンパイルしてa.outを作るとそれに全部がコピーされた。X11は自分自身(とlibc)以外に依存するライブラリもなく、コンパイル後リンクが正常にできれば、あとから問題が起こるということはなかった。
そのうちシェアドライブラリlibxxxx.so(macOSだとlibxxxx.dylib)が考え出された。実行形式ファイルのサイズとメモリ圧迫の両方を軽減することができるアイデアである(厳密に言えば別々に解決するアイデアが合体したものである)。このために、実行形式のバイナリファイルはコードが呼んでいる関数などの名前を書いておくだけでコピーせず、ライブラリの方はメモリの位置に依存しないようなバイナリになっている。実行されるときにリンクローダは必要なライブラリの実行コードをメモリに配置して、関数の名前の表にその位置を書き込んでから、main()関数を呼び出す。
実行形式ファイルの中にはその名前の表と、どのライブラリに依存しているか、そのライブラリがファイルとしてどこにあるのか(パス)、がリンカによって書き込まれる。さらにライブラリがまた他のライブラリに依存している場合には、同じことが入れ子に書かれることになる。それがローダによって全て解決できて初めてコードが実行される。
いいアイデアなんだけど、非常に複雑になった。コンパイル時点と実行時でライブラリの場所が変わったりするとローダがわからない、しかしmain()関数を含んだ実行形式ファイルは別のシステム上で動作させることも考慮する必要があって、全てのシステムでライブラリの位置が同じになっているとは限らない。
さらに、シェアドライブラリの場合、それを呼び出すコードとは別にバージョンを設定できる。呼び出し形式が一致していればライブラリだけをバージョンアップすることができるが、微妙な互換性の問題も発生する。ある実行ファイルではあるバージョンのライブラリでないと動作しない、などということも起こり得る。
そういったもろもろの問題を一気に解決できるアイデアは今の所ないらしくて、ライブラリの指定の仕方を複数用意して、それぞれの間に優先順位をつけることで、柔軟性を持たせるということがされているようである(macOSではもう少し賢い方法(Framework)が取られているが、実際にはそれほど有効に機能していないように見える)。
具体的にいうと、おおまかには優先する順番で
- バイナリに直接書き込まれた場所(パス)
- リンカ/ローダへのオプションによる指定
- /etc/ld.so.confによる指定
- いちばん一般的な/libと/usr/lib
従って、うまくいっていれば何の問題もないが、いったん問題が発生すると原因の特定が非常に難しくなる。
ここで、ほかとなんとなく違っている/etc/ld.so.confについて。なんか/etcなんてディレクトリにあるのがちょっと変だけど、構造が他のrcなんかと同じだ、ということなんだろう。/etc/ld.so.confの中身は
include /etc/ld.so.conf.d/*.confだけで、つまりld.so.conf.dの中身を参照しろ、となっている。この中には個別の設定を入れる。ld.so.conf.dの中はRaspbianだと
-rw-r--r-- 1 root wheel 12 8 12 2017 00-vmcs.conf -rw-r--r-- 1 root wheel 74 6 16 2017 arm-linux-gnueabihf.conf -rw-r--r-- 1 root wheel 41 1 17 2017 fakeroot-arm-linux-gnueabihf.conf -rw-r--r-- 1 root wheel 44 3 21 2016 libc.confとなっている。例えばarm-linux-gnueabihf.conf は
# Multiarch support /lib/arm-linux-gnueabihf /usr/lib/arm-linux-gnueabihfなどとなっている。さらに/etc/ld.so.cacheというキャッシュがある。これはldconfigというユーティリティで作るらしいけど、これは/etc/ld.so.confで設定された内容をパースして表の形にしたものらしい(binutilsにldconfigは提供されていないようだし、ネイティブのldconfigで作っても意味がないので、クロス用は作ることができない)。 こういうのはわかりやすくて変更もしやすいんだけど、優先順位は低く設定されている。
ということで、このどこかに必要なライブラリのパスがあればリンクは成功する。ところがクロス環境の場合、話がややこしくなる。パスは基本的に絶対パス指定されるようである。それが不便なのでsysrootのメカニズムが作られたんだけど、その大元のリンカローダのパス指定は悩ましい。オプションは絶対パス指定でそれはいいというかしょうがないんだけど、例えばライブラリがさらに他のライブラリを参照している場合、うまくいかなくなることがある。
セーフティネットとしての/etc/ld.so.confもクロス環境ではホストの/etcを参照してしまうので、機能しなくなる。sysrootの下に/etcを作っても当然機能しない。従ってクロス環境では環境変数で指定するかオプションでいちいち指定するしかなくなる。
ところがなぜか環境変数LD_LIBRARY_PATHに指定しても無視されてしまう(環境変数はライブラリの入れ子には影響しないということかもしれないけど、binutilsリンカのmanページを読んでもよくわからない)。sysrootメカニズムの思想の問題かもしれないんだけど、そこまで突っ込めていない。どのみち環境変数は効果が副作用的で、あまり好きではないので構わないといえばそうなんだけど。
結局オプションでいちいち指定するしか今のところ解決策がない。具体的にはリンカに直接
-Wl,-rpath-link,/usr/local/raspbian-sdk/sysroot/lib/arm-linux-gnueabihf:/usr/local/raspbian-sdk/sysroot/usr/lib/arm-linux-gnueabihfとしてクロスライブラリの場所をオプションに指定するとリンクできた(linaroのgccでそんなことしなくても動くのは、おそらく専用の記述がどこかにあったんだろう。逆にそういうのが多いせいでバージョンを上げるのさえ苦労することになった)。EclipseではプロジェクトのPropertiesの「C/C++ Build」の「Settings」の「Tool Settings」の「Cross GCC Linker」の「Miscelleneous」の「Linker Flags」(なんて深いんだ)に上の文字列を入れる と動く。すごいイマイチなんだけど、ほかにいい方法が見つからない。詳しい人、教えて欲しい。
でもなんでライブラリに入れ子がない場合はなにもしなくてもリンクできるんだろう。それもよくわからない。これも専門家の人、教えて。
2018-10-10 21:46
nice!(0)
コメント(0)
コメント 0