static link について

案外、 static link ってわかってないもんです。というかリンカってわかってないもんです。そして案外はまるものです。以下のクイズに答えられるでしょうか。

クイズ1

$ nm main.o  # int main() {}
0000000000000000 T main
$ nm foo.a   # void foo() { bar(); } void baz() {}

foo.o:
                 U bar
0000000000000010 T baz
0000000000000000 T foo
$ nm bar.a   # void bar() {} void baz() {}

bar.o:
0000000000000000 T bar
0000000000000006 T baz
$ gcc main.o foo.a bar.a

最後のコマンドで、何が起きますか?

  • 普通にリンクできる
  • undefined reference to `foo'
  • undefined reference to `bar'
  • multiple definition of `baz'

クイズ2

$ nm main.o  # int main() { foo(); }
                 U foo
0000000000000000 T main
$ gcc main.o foo.a bar.a

クイズ1との違いは main.o だけです。最後のコマンドで、何が起きますか?

  • 普通にリンクできる
  • undefined reference to `foo'
  • undefined reference to `bar'
  • multiple definition of `baz'

クイズ3

$ nm main.o  # int main() { bar(); }
                 U bar
0000000000000000 T main
$ gcc main.o foo.a bar.a

クイズ1,2との違いは main.o だけです。最後のコマンドで、何が起きますか?

  • 普通にリンクできる
  • undefined reference to `foo'
  • undefined reference to `bar'
  • multiple definition of `baz'

クイズ4

$ nm main.o  # int main() { bar(); }
                 U bar
0000000000000000 T main
$ nm foo.a   # void foo() { bar(); }

foo.o:
                 U bar
0000000000000000 T foo
$ nm bar.a   # void bar() { foo(); }

bar.o:
0000000000000000 T bar
                 U foo
$ gcc main.o foo.a bar.a

最後のコマンドで、何が起きますか?

  • 普通にリンクできる
  • undefined reference to `foo'
  • undefined reference to `bar'
  • multiple definition of `baz'

クイズ5

$ nm main.o  # int main() { foo(); }
                 U foo
0000000000000000 T main
$ gcc main.o foo.a bar.a

クイズ4との違いは main.o だけです。最後のコマンドで、何が起きますか?

  • 普通にリンクできる
  • undefined reference to `foo'
  • undefined reference to `bar'
  • multiple definition of `baz'

クイズ6-10 (飽きたので雑な出題)

クイズ1-5のそれぞれのケースについて、

$ gcc main.o foo.o bar.o

として foo.a と bar.a のかわりに foo.o と bar.o をリンクした場合、何が起きますか?

おまけ

クイズ1-5について、

$ gcc foo.a main.o bar.a

$ gcc foo.a bar.a main.o

にした場合、いろいろ挙動が変わりますが、なぜでしょう。 1-10 に正解できる人はこれも正解できると思われるのであまりクイズにする意味は無い感じなので、おまけです。

解答

リンカの .a や .o に対しての挙動について知っていれば、ラクに全問正解できると思います。全問正解できなければ、いくつあってても、あまり大差無くわかってない、という感があります。

正解は…

  • クイズ1: 普通にリンクできる
  • クイズ2: multiple definition of `baz'
  • クイズ3: 普通にリンクできる
  • クイズ4: undefined reference to `foo'
  • クイズ5: 普通にリンクできる
  • クイズ6: multiple definition of `baz'
  • クイズ7: multiple definition of `baz'
  • クイズ8: multiple definition of `baz'
  • クイズ9: 普通にリンクできる
  • クイズ10: 普通にリンクできる

です。いやー何故か偉そうな文体で書いてるから、こういうの間違ってると恥ずかしいから確認した…

説明

まず、リンカのコマンドラインは基本的に左から右に眺めていきます。 .o ファイルがあると、その中にある全てのシンボルをリンクします。クイズ6-8はこの理由で単に baz が2回出現してしまっていて、クイズ9-10は循環参照があるけど両方ちゃんと定義してあるから大丈夫、ということになります。

.a は知らない人にはやや意外性のある挙動を示します。 .o を眺める時、未定義だったシンボルの一覧をリンカは覚えています。 .a に出くわすと、その中の各 .o を眺めてみて、未定義だったシンボルの入ってる .o を全てリンクします。この際、 .o についでに入っている、使用されてないシンボルなんかもリンクされます。

クイズ1では、 main.o を見て未定義シンボルが無いので、 foo.a と bar.a からは何もリンクされず、 main だけのバイナリができます。

クイズ2では、 main.o を見て foo が未定義なので、 foo.o をリンクします。すると bar が未定義なので、 bar.o もリンクします。そして baz が多重定義になります。

クイズ3では、 main.o を見て bar が未定義なので、 bar.o をリンクします。 foo.a はスルーされるので、 main, bar, baz が定義されたバイナリになります。

クイズ4,5 は循環参照です。 4 の方は main.o と bar.o だけを取ってきてしまって foo が未定義となり、 5 では main.o => foo.o => bar.o と芋蔓式に取ってくる必要ができて、無事リンクできます。

循環参照を解決する技

普通、クイズ1-3のようなケースで悩むことはあまり多くはなくて、4-5のような循環参照で悩むことが多いのではないかと思います。これを解決する方法はいくつかあります。クイズ4のケースを例に使います。

$ nm main.o  # int main() { bar(); }
                 U bar
0000000000000000 T main
$ nm foo.a   # void foo() { bar(); }

foo.o:
                 U bar
0000000000000000 T foo
$ nm bar.a   # void bar() { foo(); }

bar.o:
0000000000000000 T bar
                 U foo
$ gcc main.o foo.a bar.a
bar.a(bar.o): In function `bar':
bar.c:(.text+0xa): undefined reference to `foo'
collect2: error: ld returned 1 exit status
素人くさい方法

まず、よく使われているであろう、 undefined reference が出るまでひたすら .a を足していくという技。

$ gcc main.o foo.a bar.a foo.a

個人プロジェクトとかでは別にいいと思います。わかりやすいですし。ある程度大きい規模の開発ではこの方法ではスケールしないんでないかと思います。

-u

シンボル foo は未定義なんだよ、と明示的に教えてやる方法。リンカオプション -u でリンカに未定義なシンボルを教えてやることができます。 -Wl を使うと gcc はリンカにオプションを伝えてくれます。

$ gcc main.o -Wl,-ufoo foo.a bar.a

foo.a と bar.a をセットでライブラリとして提供してる場合で、明確に拾いたいシンボルがわかってる場合は、この方法が一番リンクにかかる時間とかが少なくて良いと思われます。例えば SDL_main のような。ライブラリとかで無い場合はシンボルの一覧を管理するのがめんどくさいと思われます。

--start-group

最初に書いたような、ひたすら .a を足していく、というのを自動的にやってくれるオプションがあります。 --start-group と --end-group がそれ。

$ gcc main.o -Wl,--start-group foo.a bar.a -Wl,--end-group

とすると、未定義シンボルがなくなるまで、 foo.a と bar.a を循環してくれます。昔は濫用すると遅かったようですが、今ではあまり気にならないのではないかな…と思います。

--whole-archive

少し毛色が違うものですが、この .a は .o みたいな感じで全部シンボル拾ってきておくれ、と指示するオプションがあります。

$ gcc main.o -Wl,--whole-archive foo.a bar.a -Wl,--no-whole-archive

ただ、あまりこの用途で使うことは無いと思います。

よくあるのは、 shared object を作る時に使うケースではないかと思います。さっきの foo.a から foo.so を作ることを考えます。

$ gcc foo.a -shared -o foo.so
$ nm -D foo.so
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w _Jv_RegisterClasses
0000000000200830 B __bss_start
                 w __cxa_finalize
                 w __gmon_start__
0000000000200830 D _edata
0000000000200838 B _end
00000000000005e8 T _fini
00000000000004a8 T _init

大変です。 foo が入ってません。これは基本ルールを思い出すと簡単で、 foo.a を眺めた時に、特に未定義なシンボルが無いので foo.a がスルーされたというだけの話です。

こういう、この .a に入ってるシンボル全部入ってる .so 作ってくれーという時に --whole-archive は便利で、

$ gcc -Wl,--whole-archive foo.a -Wl,--no-whole-archive -shared -o foo.so
$ nm -D foo.so | grep foo
0000000000000685 T foo

などと。無事入ってました。

shared object

はまぁ割とみんなが想像するような挙動をするというか、リンカの段階では難しくなくて、ローダの側でやや難しい感じですね。

libc.a

libc.a に入ってる .o が、1関数につきひとつあるみたいな状態なのは、 static link した時のバイナリサイズを最小にするため、というようなことが予想できますね…

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h