第7章 リンク
7章について
この章では、そもそもリンクとはなんだろう?ということから始まり、静的リンクと動的リンクの違いや効率化の手法について説明されていました。
リンカが行っている動作について学ぶことで、リンクエラーを避けることができ、大きなプログラムを作成するときに便利になります。
以下まとめです。
リンクとは?
データを一つのファイルにまとめ上げ、メモリ上にコピーし実行できるようにすること。
コンパイル時のほか、ロード時や実行時にもプログラムによって行うことができる。
(例) main.o, func.o, lib.a など必要なものをまとめて a.out を作成する流れのこと。
静的リンク
静的リンカは再配置可能オブジェクト・ファイルおよびコマンド・ライン引数を入力として受け取り、ロードして動かすことのできるリンク済みの実行可能オブジェクト・ファイルを生成する。
実行可能ファイルを生成するためにシンボル解決と再配置の2つの作業を行う。
- シンボル解決 : オブジェクト・ファイルはシンボルを定義・参照しているので、それを元に適切な処理をする。
- 再配置 : 各シンボルの定義にメモリ位置を対応づけ、そのシンボルへの参照が対応づけたメモリ位置を指すようにする。
オブジェクト・ファイル
3種類存在する。
- 再配置可能 : コンパイル時に他の再配置可能オブジェクト・ファイルと統合し、実行可能オブジェクト・ファイルを生成可能。
- 実行可能 : 直接メモリにコピーし実行可能。
- 共有 : ロード時もしくは実行時にメモリに関して読み込み動的にリンクできる。
再配置可能オブジェクト・ファイル
主要なセクション
- .text: 機械語
- .data: 読み書き可能なデータ(初期化されているグローバルおよび静的変数)
- .rodata: リードオンリーのデータ
- .bss: 初期値が 0 のデータ (初期化されていないグローバルおよび静的変数)
-g
オプション付きで存在するセクション
- .debug: ローカル変数および型のエントリを含む
- .line: ソースの行番号と機械語との対応関係を持つ
-g
オプションをつけずにコンパイルするとステップ実行ができなくなる原因は、このセクションを見ると理解できますね。
シンボル
再配置可能オブジェクト・モジュールには、シンボルに関する情報を含むシンボル・テーブルが存在する。
大域シンボル : 他のモジュールから参照可能な(静的でない関数およびグローバル変数)
外部的な大域シンボル : 他のモジュールで定義されている
局所シンボル : そのモジュールでのみ定義・参照される (静的な関数および static 属性付きで定義されたグローバル変数)
シンボル解決
シンボルへの参照を、シンボル・テーブル内にあるシンボルの定義から一つ選び対応づけることで解決する。
局所シンボルの場合 : 局所シンボルの定義はモジュールごとに一つしか存在しないので簡単。
大域シンボルの場合 : 現在のモジュール内で定義されていないシンボルを見つけたとき、そのモジュールを外部へ探しに行く。リンカが参照されているシンボルの定義を見つけられなかったらエラーとなる。
strongシンボル: 関数および初期化済のグローバル変数
weakシンボル: 初期化されていないグローバル変数
以下のルールをもとに解決する。
- 同名の strong シンボルが複数存在してはならない。
- 同名の strong シンボルと weak シンボルが存在する場合、strong シンボルを選択する。
- 同名の weak シンボルが複数存在する場合、任意の一つを選ぶ。
解決できない場合はエラーが発生する。
二重定義
// foo.c int x = 10; // bar.c int x; 宣言だけならエラーにならない // hoge.c int x = 1; // strongシンボルの二重定義でエラーになる。
グローバル変数によるバグ
// foo.c int y = 1000; int x = 1000; // foo.c内ではint // bar.c double x; void f() { x = -0.0; // bar.c内ではxはdoubleとして扱われる }
bar.c の x = -0.0
によって x と y のメモリ領域が倍精度浮動小数点数点表現された負のゼロで上書きされてしまう。
そのためx = 0x0 y = 0x80000000
という結果になる。
マングリング
C++ などではソース・コード内に同名の引数が異なるメソッドの定義を許している。 同名の関数や変数を識別するために名前をエンコードすることをマングリングとよぶ。
マングリングの例
int foo(int) {} // _Z3fooi int foo(int, int, float) {} // _Z3fooiif
例では、引数の頭文字を付加していますが、実際にはもう少し複雑な名前付けを行っています。
ライブラリが必要な理由
Pascal では標準関数が少ないため、コンパイラが関数の呼び出しを認識して適切なコードを直接生成する。
しかし、C言語など標準関数が多いものに関しては、そのやり方は向いていない。
なぜなら関数の追加や削除、修正するたびに、コンパイラのバージョンを新しくする必要があるためである。
そのため静的ライブラリ .a
を活用する。
またライブラリを指定する際の原則はコマンド・ラインの末尾に置くこと。
(例)foo.cが lib1.a, lib3.a を呼び出し、それが lib2.a を呼び出す場合
gcc foo.c lib1.a lib3.a lib2.a
とする必要がある。
静的ライブラリの欠点
定期的に保守、更新する必要がある。
またライブラリの更新するたびに、再リンクする必要がある。
再配置
リンカがシンボル解決ステップを完了すると、コード内の各シンボルへの参照にただ一つの定義が対応づけられる。
セクションおよびシンボル定義を再配置 : 同じ型のすべてのセクションを統合し、一つの修正セクションにする。
セクション内のシンボルへの参照を再配置 : コード及びデータ・セクション内のシンボルへの参照を修正し、正しい実行時アドレスを指すようにする。
シンボルへの参照を再配置する : 相対アドレスまたは絶対アドレスを用いて重複する再配置する。
共有ライブラリって?
静的ライブラリの欠点を補うために生まれた。
実行時、もしくはロード時に任意のメモリ・アドレスに読み込み、メモリ上のプログラムとリンクすることができる。
複数プロセスが同じライブラリのコードをメモリ上で共有することで資源を節約できる。
.so
拡張子によって識別される。
共有ライブラリの実用
- ソフトウェアの配布 : ユーザがアプリケーションを実行すると、新しい共有ライブラリが自動的にリンクおよびロードされる。
- 高性能ウェブ・サーバの構築 : 口座残高、バナー広告などの動的コンテンツ。
共有ライブラリの効率化
共有ライブラリのメモリ資源節約のため、あらかじめアドレス空間内のある領域を各共有ライブラリに割り当てておく。
しかし、ライブラリを使用しない場合でも領域を確保するため空間効率が悪い。
また管理が難しく、割り当てた領域が重ならないように保証しなければならない。
これらの問題を避けるため、メモリ上のどこにでもロードできる形式(PIC)にコンパイルする。
ポジション非依存コード(PIC)
再配置なしにロード可能なコードのこと。
PIC としてコンパイルされた共有ライブラリは任意の位置にロード可能であり、実行時に複数のプロセスで共有できる。
PIC は PLT のように相対アドレッシングのみで構成されていていて、再配置可能なコード。
PIC関数呼び出し
コンパイラには関数の実行時アドレスを予測する方法がない。
通常の方法では、参照の再配置する再配置レコードを生成し、プログラムがロードされたときに動的リンカが解決できるようにする。
GNU では遅延束縛という技術を用いているこの問題を解決している。
遅延束縛
関数のアドレスの解決をそれが実際に呼び出されるまで遅延することで、動的リンカはロード時に不要な再配置を避ける。
関数が最初に呼び出されるときにはコストがかかるが、その後の各呼び出しは低コスト。
言語束縛
多くのライブラリは、C言語や C++ などのシステムプログラミング言語で書かれているが他の言語でも使えるようにしたもの。
ライブラリそのものを複数の言語で作成するよりも効率的となる。
またはシステム言語と同じ機能を高級言語で記述できないためという場合もある。
インターポジショニング
共有ライブラリの関数への呼び出しに介入し、代わりに独自のコードを実行できるようにすること。
特定のライブラリ関数のラッパー関数を作成して、全く別の実装に置き換えたりする。
コンパイル時やリンク時、実行時に行うことができる。
感想
オブジェクト・ファイルやライブラリをまとめる上で、内部的にどのように動作しているのか知ることができました。
シンプルに見えて、複雑なプロセスを経ていることが学べて良かったです。
個人的にこれまでは静的ライブラリを利用することが多かったですが、動的ライブラリや共有ライブラリなど、用途に合わせて使ってみようと思いました。