ubuntu 16.04 LTS日本語版で共有ライブラリを作る時にハマったこと

  • 7
    Like
  • 0
    Comment

1.はじめに

こんにちは。

先日、C++用のシリアライザ(Theolzier)オープンβとして公開しました。
その公開前にTheolizerをlinux対応したのですが、その際に思わぬ落とし穴にハマりました。日本で発生し易いものなのでご報告します。

現象は下記です。このメッセージだけでは 何をすれば回避できるのか見当が付かないので頭痛いです。

ubuntu 16.04日本語版で、プリインストールされたgcc 5.4.0を使って共有ライブラリをビルドするとld(gccのリンカ)が下記メッセージで落ちる場合があります。

collect2: fatal error: ld terminated with signal 6 [中止], core dumped
compilation terminated.
*** invalid %N$ use detected ***

原因はubuntu日本語版にプリインストールされたgccのメッセージの日本語訳文字列の不具合と思われます。
対策は-fPICオプションをつけてビルドすることです。

なお、不具合ですから、これを読まれている時には既に修正されているかも知れません。
「-fPICをつけてリコンパイルしろ」の旨のエラーメッセージが出る場合は修正されているのだと思います。

2.状況

Theolizerはアプリとリンクするためのライブラリを含みます。
これはboostのfilesytemを使っています。boost::filesystemはヘッダオンリでは使えないため、自力で最低限のオプションでビルドし静的リンクしています。
そして、Theolizerライブラリは静的リンクと動的リンク(共有ライブラリ)の両方を提供します。

WindowsのMinGW 5.4.0で共有ライブラリのビルドと自動テスト、ubuntu 16.04のgcc5.4.0で静的リンク・ランブラリのビルドと自動テストにPASSすることを確認できてました。

この状態でubuntuで共有ライブラリをビルドしようとしました。あっさり成功することを期待していたのですが、冒頭のエラーでリンカが落ちるのです。これは頭痛いです。

3.原因と-fPICオプションについて

Google先生に*** invalid %N$ use detected ***で聞いてみたところ、こんな情報が見つかりました。

*** invalid %N$ use detected ***
Aborted (core dumped)
Format string positional values are being skipped, which means their type (and size on the stack) cannot be checked. This could cause unexpected results including stack content leaks, especially when using %n. This is invalid, for example: printf("%2$s\n", 0, "Test"); because position 1 is skipped.

*** invalid %N$ use detected ***は引数順指定printfの書式(後述)が不正な時である旨書かれています。
つまり、ldが何かメッセージを出そうとして、その日本語訳文の引数順指定書式にバグがあり落ちているものと推定できます。(答えが分かってから書いているのでスムーズに見えますが実際には結構苦労しました。)

ならば、ロケールを英語にすれば普通にそのメッセージが読めるはずです。
早速、export LC_ALL==Cして再度ビルドしたところ、下記エラーでした。

libboost_filesystem.a(operations.o): relocation R_X86_64_32 against `.rodata.str1.1' can not be used when making a shared object; recompile with -fPIC

なるほど、boost::filesystemを-fPIC付けてビルドしろってことですね。
確かにboostのビルド時-fPICは指定していませんでした。

といいますか、この時はまだ-fPICオプションって何?でした。

-fPICオプションについて調査

再度Google先生に聞いてみたところ、linuxの共有ライブラリは-fPICオプションを付けて「位置独立」になるようコンパイルした方が良いことが判りました。

でも、必須じゃないようです。なのにldが出したのは警告ではなくエラーです。う~ん。

今度は64bitも付けて聞いてみました。英語しか見つかりません。
Why does gcc force PIC for x64 shared libs?(何故gccは64bit共有ライブラリにPICを強制する?)への回答で更にRe: Non-PIC objects in shared lib still works?(非PICな共有ライブラリは今も動くのか?)を参照してました。
その回答が下記です。

Shared libs need PIC on x86-64, or more accurately, relocatable code
has to be PIC. This is because a 32-bit immediate address operand
used in the code might need more than 32 bits after relocation. If
this happens, there is nowhere to write the new value. Theoretically,
it would be possible to generate non-PIC code without using the
troublesome 32-bit immediate operands, but to my knowledge no compiler
does this.


訳というか私が理解した内容
gccは64ビット・ビルド時でも直接アドレスする時は32ビット・モードを使うから、共有ライブラリ等のリロケータブル・コードではアドレスのビット数が足りない。PLT(Procedure Linkage Table)を経由すればこの問題を回避できるので-fPIC指定が必要ということのようです。
64ビット・モードなら32ビット・モードを使わなければ良いのにとも思いますが「理論上それは可能だが、私の知る限りそのようなコンパイラはない」そうです。


即値でアドレスするのはグローバル領域やコード領域なので再配置しないなら32ビットあれば十分ということなのかも知れません。

PLTについては分かり易い解説を見つけることができませんでした。
知りたい方は"PLT GOT"等で検索してみて下さい。

引数順指定

標準規格にはないようですがlinuxには引数順を指定できるprintf書式があります。これは多言語化の時便利です。
例えば、"Please refer to Section 3 in 15 pages."のようなメッセージの315をバラメータにしたい場合、下記のようにします。
printf("Please refer to Section %u in %u pages.", section_no, page_no);

そして、gettext方式で多言語化する時に引数順指定書式が有用なのです。
自然な日本語としては、「15ページの第3節を参照下さい。」のように訳したいです。
15と3の順序が逆なので標準のprintf書式では対応できません。しかし、引数順指定を使えば下記のように対応できます。
英文をprintf("Please refer to Section %1$u in %2%u pages.", section_no, page_no);としておき"Please refer to Section %1$u in %2%u pages."に対する日本語訳を"%2$uページの第%1$u節を参照下さい。"とするわけです。

C++で引数順指定を使いたい時、boost::formatが便利です。
C++でgettestを使いたい場合、boost::localがあるのですが、これはイマイチ使いづらいです。

4.対策

原因が分かれば対策は明らかですね。boost等、静的リンクするライブラリを-fPICオプションを付けてリビルドすればOKです。
Theolizerも無事ビルドでき自動テストに通りました。

さて、自分自身(私の場合Theolizerライブラリ)は-fPIC指定しなくてよいのか?というと、当然必要です。
私はTheolizerライブラリのビルド・プロジェクト生成するためにCMakeを使っているのですが、CMakeが共有ライブラリの時には自動的に-fPICオプションをつけてました。

CMakeは本当に強力です。かゆいところを知らない内に掻いてくれます。

5.最後に

実はlinuxでのライブラリ開発は初めてです。昔カーネルを読みつつドライバにちょこっと手を入れてAndroid(Almadillo)を動かしたくらいです。なので、今回は予想外の事態にそこそこ苦労しました。

当初よりマルチ・プラットフォーム設計し(OSのAPIを直接使わず、必要な部分はboost等で吸収する)、MSVCだけでなくgccのWindows版であるMinGWでも動作させました。
これによりlinuxへの移植はあまり苦労しない筈と踏んでました。

確かにその御蔭でTheolizerのコア部分はほとんど何も対策せずに済みました。
しかし、PIC問題等、細かい部分ではそこそこ苦労しました。多くはboost, CMake, libToolingの微細な振る舞いの相違、および、Windowsとlinuxの運用上の習慣の相違です。

当たり前ですが、マルチ・プラットフォームはやはり結構たいへんですね。