まっつーのブログ

本の感想や振り返りなど雑多に書いてます

植山類さん講演会

植山類さんの講演会に参加してきました。

植山さんは、高速なリンカlldのオリジナル作者かつ現メンテナで、Cコンパイラ8ccの作者でもあります。

そのため今回は特にリンカについて詳しく教えていただきました。

リンカの役割から動的リンクの仕組みまで前提知識がなくても分かりやすいように解説してくださいました。

ここから、学んだことをまとめています。

リンカの役割

オブジェクト・ファイル(.o)をつなぎ合わせて単体の実行ファイルや、共有ライブラリ(.so)にすること。
使用例 : cc -o hello hello.o main.o

リンカのコマンド(ld)も、cc 経由で間接的に起動するのが普通の使い方だという。
ちなみにcc はコンパイラではなくコンパイラドライバらしい。

-### オプションをつけることで実際に cc がどのように ld を起動しているのかを確認できる。
使用例 : clang -o hello hello.o main.o -###

スタートアップルーティン
  • 普通アンダースコアから始まる。

  • _start みたいな感じ。

リンカが使われる場合

オブジェクトファイルは セクション という単位に分割することができる。

オブジェクトファイルの情報の表示
objdump -s hello.o

  • Hello world. みたいな単なる文字列は .rodata (readonly) セクションに位置する。

機械語の入っている .text セクションの逆アセンブル
objdump -d hello.o

主要なセクション
  • .text: 機械語

  • .data: 読み書き可能なデータ

  • .rodata: リードオンリーのデータ

  • .bss: 初期値が0のデータ (初期化されていないグローバル変数)

  • call 命令のプレースホルダからのオフセット値が0だから次の命令を指すことになる。(この辺は難しかった)

実行ファイル
  • 実行ファイルにもコードとデータが分かれて入っている。

  • メモリ領域の上半分はカーネルが使うらしい。

イメージ (下の方がアドレスが若い)

[メモリ]
カーネル
スタック
ヒープ領域
0初期化されたデータ
初期化済みデータ
コード

スタックオーバーフローの仕組み

スタック領域は上の方にあって、使うごとに下に伸びる。
関数の呼び出しを繰り返して深くなりすぎると、メモリがマップされていない領域にアクセスしてしまい、セグフォする。

そんなに下に行くとデータを上書きしてしまうってところでストップをかけてくれる。
そんなに再起回っていたら無限再起だろー。って判定してくれるらしい。

mallocで確保できるサイズより小さいサイズでスタックがセグフォする理由

mallocで 4GB 確保できても スタックでは 2GB でセグフォする。
理由としては、技術的には可能だが普通はプログラムエラーなのであえて終わらせてるらしい。

カーネルのパラメータを変えれば大きいスタック領域も確保できるらしい。

ちなみに32bitマシンでは、スタック領域がそんなに取れないが、64bitマシンはメモリ領域が広いのでもしかしたら動作が早くなるかもしれないという。
なぜなら、stackポインタの移動にはそこまでコストはかからないから。mallocとfreeはコストがかかる。

リロケーション

オブジェクトファイルはでプログラム全体の一つの断片で、それ単体では実行できない。

リンカが複数のオブジェクトファイルを 1つにまとめる。(リロケートするという。)

コンパイラが関数をコンパイルするときには関数がが実行時にどこに存在するのかわからないので、アドレスを埋められない。

  • objdump -dr hello.o でリロケートの流れが見える。
リロケーションレコード
  1. どの場所を修正するか。

  2. どのアドレスで修正するか。

  3. どのように修正するか。

リンカの起源

1947年にリンカっぽいものができた。(アセンブリが発明されるよりも前。)

機械語で直接コードを書いているとしても、リンカは存在しないと困ってしまう。
なぜならsortなどの汎用性があるコードを書いて再利用するのに便利なので。

アーカイブファイル

スタティックライブラリ (アーカイブファイル .a ) は、 .oファイルをまとめたもの。

実行例 : ar t /hoge/libc.a

  • t をつけると tar みたいに展開して実態が見れる。

libc.a には、libc の関数が別々の .o ファイルに小分けされて入っている。
これは元々の libcのソースコードのレベルから、別のファイルに分けられている。
それらをコンパイルして最後にまとめて .a にまとめている。

必要なファイルだけが勝手に選ばれる仕組みが必要でアーカイブファイルが生まれた。
手動だとプログラムがどのオブジェクトファイルに依存しているかを分けるのが大変なので。

.oを小分けすることで使う関数だけをアーカイブファイルから取れるのでメモリの節約になる。

ダイナミックライブラリ

大きいコードを別々の実行ファイルにも埋め込むのは無駄なのでダイナミックライブラリを活用する。

しかしそこまで劇的に性能が上がるってわけでもないらしい。
理由としてはダイナミックライブラリを呼び出すのにもオーバーヘッドが生じるから。

再リンクをせずにコードだけをアップデートのしたいときには便利である。

使い方としては、実行ファイルとライブラリを別ファイルにわけて、ロードするときにメモリ上で組み合わせる。

実行例 : objdump -T libc.so(これで流れが確認できる)

ちなみにダイナミックリロケーションすることでロード時に書き換える場所が多くなるのでプログラムの起動が遅くなる。
ページ共有ができなくなるので物理メモリの使用量が増える。

GOT/PLT

ダイナミックライブラリを参照しているシンボルのためにGOT/PLT作る。

ライブラリの関数やデータを参照している場所を一箇所に集約することでダイナミックリロケーションのデメリットを解消する。

直接、アクセスするのではなく、 PLT, GOTを用いる。

  • 相対的なアドレスは常にメモリ上で固定なので常に同じ命令で参照できる。

ローダーのためにダイナミックリロケーションを作る。

GOT/PLTを使わないシステム

昔のバイナリフォーマット a.out には GOT/PLTはなかった。
共有ライブラリは特定のライブラリアドレスにロードされるのが前提にリンクされていたという。

オーバーヘッドは a.out 方式の方が小さい。

a.out フォーマットの問題

2つの共有ライブラリのアドレスが重なっているとロードできない。
そのため共有ライブラリのを配布する場合、適当なアドレスを予約してそれを他の人が使わないように登録しておくといった重複排除の仕組みが必要となる。

しかし共有ライブラリのサイズが成長していって結局他のもとかぶってしまうことが起こり得る。
そういった背景から a.out フォーマットから GOT/PLT へと移行した。

リンクエラーの原因

シンボル名が解決できない場合。(使いたい関数が存在しない)

シンボル名が重複している場合。(同名の関数が定義されている)

リンカの雑多な機能

  • 特定のアドレスにロードされるのが特定のライブラリ関数やデータを配置する。(主に組み込みやカーネルプログラムのため)

  • インライン関数など、複数のオブジェクトファイルに重複して含まれているものを一つだけ選ぶ。

  • 共有ライブラリからどのシンボルをエクスポートするかを選ぶ。

  • 誰からも参照されていないセクションを出力ファイルに入れない。

他にもいくつかあるらしい。

マングリング

C++名前空間や、同名の関数を引数の型でオーバーロードする場合の名前解決に役立つ。

  • 型や引数によって名前を変えている。

  • もし引数や関数名が被っていても上手いことやってくれるらしい。

int foo(int) {}
// _Z3fooi:

int foo(int, int, float) {}
// _Z3fooiif

上記のようにマングリングが行われる。

感想

とても勉強になりました。新しい学びが多く面白かったです。
CSAPPで事前に予習していたおかげで全体の流れが理解できた気がします。 また今回学んだことは今後の学習に役立てていこうと思います。
貴重な機会を提供してくれた42 Tokyoの皆さん、公演していただいた植山類さんありがとうございました!