ターゲットファイルの中身は?
リンカがターゲット・ファイルのアドレスと領域を確保することである:
アドレスとスペースには2つの意味がある:
1. 出力される実行ファイル内の
2. をロードした後の、仮想メモリ上の仮想アドレス空間である。
例えば、後述するbssセグメントでは、ターゲットファイル内のスペースは占有しないが、仮想アドレス空間は占有するという違いがある。
焦点は仮想アドレス空間の割り当てです。
ターゲットファイルの構造
文書に記載されている順番は以下の通りです:
- ELF Header
- text
- data
- .bss
- ...
- section header table
- string tables
- symbol tables
- シンボルテーブル
ファイルのヘッダー
ELFファイルバージョン、ターゲットマシンモデル、プログラムエントリアドレスなど。
セグメンテーションテーブル
セグメント属性とは、ALLOC、LOAD、READONLY などです。セグメント属性とは、ALLOC、LOAD、READONLY などのことです。オペレーティングシステムがこれらの属性を見ることで、そのセグメントをメモリにロードするか、読み取り専用にするか、読み取り書き込み専用にするかなどを知ることができます。オペレーティングシステムにとって、セグメント名に意味はありません。
string tables
ターゲット・ファイルは非常に長いので、オペレーティング・システムが文の区切り方を知るために、ターゲット・ファイルでは2つのアプローチが取られます:
- 最初のフィールドは、セグメントテーブルのような構造体の長さを表しますが、これは実際には配列として解釈することができ、配列の最初の要素にその長さを表します。
- セグメント名は文字列であるため、固定長は、例えば、セグメントテーブルの要素は、フィールドがセグメント名であり、固定長ではないので、文字列を置くために特別な文字列テーブルを提供し、セグメント名は、その上に文字列テーブルのアドレスにポイントを格納する必要があります。
セグメントに文字列テーブルが必要な場合は、そのセグメント用に別のセグメントが作成され、異なるセグメントが混在することはありません。
コードセグメント
ソースコードのどこに関数が格納されているか
データセグメント、読み取り専用データセグメント、BSSセグメント
静的データは、場所、アクセス制御、初期化されているかどうかに応じて、別々の場所に配置されます:
- BSS:初期化されていないグローバル/ローカル、読み書きデータ
- .rodata: 初期化されたグローバル/ローカル、読み取り専用データ
- .data: 初期化されたグローバル/ローカルな読み書き可能データ
なぜこのような分配を行うかというと、まず、読み出し専用と読み出し書き込み専用は別のセグメントに分ける必要があります。仮想メモリのアクセス制御はページに対して正確であるため、つまり、各ページのアクセス制御は同じであり、同じセグメントのデータを同じページに入れる可能性があるため、分ける必要があります。
次に、初期化されたデータについては、ターゲット・ファイルに格納する必要がありますが、初期化されていないデータについては、格納する必要はありません。 初期化されていないデータについては、ターゲット・ファイルに領域を確保する必要はありませんが、プログラムをロードする際に、オペレーティング・システムが適切な領域を確保しやすいように、bss段落で合計でどれだけの領域が必要かを宣言しておく必要があります。
ただし、グローバルに初期化されていないスタティック変数は、後で説明する理由により、ターゲット・ファイルの bss セクションではカウントされません。
シンボルテーブル、再配置テーブル、GOTテーブル
詳しくはリンク先をご覧ください。
リンク
シンボル
リンキングプロセスの本質は、複数のターゲットファイルを接着して全体を形成することです。異なるターゲットファイル同士を接着させるために、これが機能するためには、これらのターゲットファイル間に固定ルールがなければなりません。
リンキングでは、ターゲットファイル間のコロケーションは実際にはターゲットファイル間のアドレス参照、つまり関数や変数へのアドレス参照です。
例えば、ターゲットファイルBがターゲットファイルAの関数fooを使用したい場合、Aは関数fooを定義したと言われ、Bはfooを参照したと言われます。
リンキングでは、関数と変数を総称してシンボルと呼びます。 シンボルの管理はリンキングプロセスの重要な部分で、各ターゲットファイルには対応するシンボルテーブルがあり、ターゲットファイルで使用されるすべてのシンボルが記録されています。
シンボルには以下の種類があります:
- グローバルシンボル:このターゲットファイルで定義され、他のターゲットファイルから参照可能
- 外部シンボル:このターゲットファイルで参照されているが、このターゲットファイルで定義されていないグローバルシンボル
- げだい
- ローカルシンボル:現在のコンパイル単位でのみ表示されます。
- 行番号情報
静的リンク
静的リンクは、コンパイルの過程で行われます。空間とアドレスの割り当て、シンボル解決と再配置の2つのステップがあります。
スペースとアドレスの割り当て
すべての入力ターゲットファイルをスキャンし、セグメントの長さ、属性、位置を取得し、入力ターゲットファイルのシンボルテーブル内のすべてのシンボル定義とシンボル参照を収集し、グローバルシンボルテーブルに配置します。
シンボルの解決と再配置
前のステップの結果に基づいて、まだアドレスが設定されていない外部シンボルを必要とするものを探し、アドレスを見つけ、それを調整します。
各ターゲット・ファイルはシンボル・テーブルと再配置テーブルを持ちます。
再配置されたセグメントのセグメント名は .rel.xxx で、xxx は再配置されたセグメントのセグメント名です。例えば、.text セグメントの再配置セグメント名は .rel.text です。
リロケーションテーブルは、どの外部シンボルがターゲットファイルのどこでどのように参照されているかを記録します。
すべてのターゲットファイルを1つのファイルにまとめた後、各シンボルのアドレスを決定し、これらのシンボルのアドレスを見つけ、再配置エントリに記録されたアドレスに従って外部シンボルの実アドレスを記入します。これで静的リンクは完了です。
動的リンク
異なるアプリケーションが同じライブラリを使用する可能性があり、各アプリケーションがライブラリを一度ロードすると、仮想メモリ領域の無駄遣いになります。
そのため、スペースを節約するために、異なるプログラムにライブラリの同じイメージを使用させる方が良いですが、このイメージのアドレスを固定することはできません、異なるプログラムが異なる空き領域を持っているため、調整することは困難であり、複数のプログラムがイメージを格納するために同じスペースを解放させ、我々は唯一の異なるプログラム空間の異なる場所にライブラリをロードすることができます。
これには次のような問題があります:
- 内部関数へのライブラリ呼び出し:次の命令のアドレスと呼び出される関数のアドレスの間隔は固定されているため、相対アドレスを取ることが可能です。
- ライブラリは内部変数を使用します。呼び出された関数と変数の間隔も固定ですが、変数へのアクセスは相対アドレス方式をサポートしていないので、次の命令のアドレスと固定間隔を取得して変数のアドレスを割り出し、そのアドレスを取得してアクセスする必要があります。
- ライブラリは、外部変数を使用して:間隔が固定されていないため、上記の2つのメソッドを使用することはできませんので、静的リンクは、再配置方法、ロードの再配置操作を取るために、それはライブラリを共有するために行うことはできません、再配置のため、コードセグメントを変更する必要があります、各プログラムは、ライブラリのコードセグメントを変更するためにロードされた、そのいくつかを使用することはできません。そこにライブラリの各使用のためのプログラムはありませんので、再配置の必要性の内容は、データセグメントに配置され、ライブラリのデータセグメントが異なっている必要がありますので、再配置の内容は、データセグメントに配置され、プログラムは、データセグメントに保存されたアドレスを変更することができます開始されます。このデータセグメントは、GOTと呼ばれ、このようなコードは、アドレスに依存しないコードと呼ばれています。
- データセグメントのアドレス
- データセグメントのアドレス
データセグメントのアドレス:
extern static int a = 10;
static void *p = &a
この変数は再配置テーブルにも記録され、ロード後に再配置されます。
遅延バインディング
ダイナミック・リンクの問題点は、プログラムの開始が遅いことなので、遅延バインディングが採用されました。遅延バインディングの実装は非常に微妙です:
liba.soがlibb.soのbar()関数を呼び出すと仮定すると、この関数のアドレスを見つけるには、少なくともモジュールIDと関数名を知っている必要があります。
この関数のアドレスを探す関数がlookup(moduleId, function)だとします。
元のbar()の呼び出しは、GOT内のbarの対応物がbar@GOTであると仮定して、直接GOTに行きました。
ロードを遅らせるために、新しいプロシージャ・リンケージ・テーブルが追加され、PLT内のbarの対応表がbar@PLTであると仮定し、bar()の呼び出しがPLTに来て関数アドレスを見つけるようになりました。
bar@PLTのコードは以下の通りです:
jmp *(bar@GOT)
push n
push moduleID
jump lookup
プログラムがロードされた後、bar@GOTのアドレスはプッシュn命令のアドレスになります。
barへの最初の呼び出しはbar@PLTにやってきて、bar@GOTにジャンプし、nをプッシュします。
nはリロケーションテーブル.rel.pltのバーシンボルリファレンスの添え字で、moduleIDがスタックに押され、lookupにジャンプし、lookup関数を呼び出します。bar@PLTにジャンプしてからbarのアドレスにジャンプします。
ELFはGOTを".got "と".got.plt "という2つのテーブルに分割します。".got "はグローバル変数参照のアドレスを保持するために使用され、".got.plt "は関数参照のアドレスを保持するために使用されます。
動的にリンクされたセグメント
静的リンクには、動的リンクと同様にシンボルテーブルと再配置テーブルが必要です。 動的リンクのシンボルテーブルは.symtabで、コードセグメントの再配置テーブルは.rel.dynです。
解析
リンク時に関数アドレスを変更する方法があります:
- 再配置:見つかったシンボルのアドレスを直接ロケーションに書き込みます。
- アドレス非依存コード
ここでいうコードセグメントの再配置とは