リンカーやローダーなどをよりよく理解しようとしています。
コンピュータサイエンスのどの分野に属していますか?コンパイラ、オペレーティングシステム、コンピュータアーキテクチャ?
開発中にリンカーとローダーはどこで機能しますか?
exactの関係は多少異なります。まず、MS-DOSなどの実行可能ファイルが常に静的にリンクされる、(ほぼ)最も単純なモデルを検討します。例として、正規の「Hello、World!」を考えてみましょう。私たちが仮定するプログラムはCで書かれています。
コンパイラはこれをいくつかの部分にコンパイルします。文字列リテラル "Hello、World!"を受け取り、定数データとしてマークされた1つのセクションに入れ、その特定の文字列の名前を合成します(例: "$ L1")。 printf
の呼び出しをコードとしてマークされた別のセクションにコンパイルします。この場合、名前はmain
(または頻繁に_main
)と表示されます。また、このコードのチャンクはNバイト長であり、(重要なことに)そのコードのオフセットMにprintf
への呼び出しが含まれていると言うこともできます。
コンパイラが生成を完了すると、リンカが実行されます。これは通常、開発ツールチェーンの一部と見なされます(例外はありますが、MS-DOSにはリンカーが含まれていましたが、使用されることはまれでした)。通常、外部からは見えませんが、通常、コマンドライン引数が渡されます。1つは起動コードを含むオブジェクトファイルを指定し、もう1つはC標準ライブラリを含むファイルを指定します。
次に、リンカはスタートアップコードを含むオブジェクトファイルを調べて、たとえば1112バイト長であり、オフセット784にある_main
を呼び出していることを確認します。
これに基づいて、シンボルテーブルの作成を開始します。 「.startup」(または任意の名前)が1112バイトの長さであるというエントリが1つあり、(これまでのところ)その名前は参照されていません。 「printf」は現在不明な長さであるという別のエントリがありますが、「。startup + 784」から参照されます。
次に、指定された1つまたは複数のライブラリをスキャンして、現在定義されていないシンボルテーブル内の名前の定義(この場合はprintf
)を見つけようとします。これは、printfのオブジェクトファイルが4087バイトであり、intを文字列に変換するような他のルーチンへの参照や、putchar
(またはfputc
)は、結果の文字列を出力ファイルに書き込みます。
リンカは再スキャンして、再帰的にこれらのシンボルの定義を見つけようとします。これは、2つの結論のうちの1つに達するまでです。すべてのシンボルの定義が見つかったか、定義が見つからないシンボルがあります。
参照が見つかったが定義が見つからない場合は、停止して通常は「未定義の外部XXX」について何かを示すエラーメッセージが表示されます。リンクする必要がある他のライブラリまたはオブジェクトファイルを特定するのはあなた次第です。 。
すべてのシンボルの定義が見つかると、次のフェーズに進みます。各シンボルを参照する場所のリストをウォークスルーし、そのシンボルがメモリに配置されたアドレスを入力します(たとえば、 )スタートアップコードがmain
を呼び出す場合、アドレス1112
をmainのアドレスとして入力します。それがすべて完了すると、すべてのコードとデータが実行可能ファイルに書き出されます。
おそらく言及すべき他のいくつかのマイナーな詳細があります:通常はコードとデータを別々に保持し、それぞれが完了した後、それらを(多かれ少なかれ)連続したアドレスにすべてまとめます(たとえば、すべてのピース)コード、次にすべてのデータ)。通常、セクション/セグメントの定義を組み合わせる方法に関するいくつかのルールもあります。たとえば、異なるオブジェクトファイルにすべてコードセグメントがある場合、コードの断片を次々に配置するだけです。 2つ以上の同一の文字列リテラル(または他の定数)が定義されている場合、それらは通常それらをマージして、すべてが同じ場所を参照するようにします。また、同じシンボルの定義が重複している場合に、何をすべきかについてのルールもいくつかあります。典型的なケースでは、これは単にエラーになります。いくつかのケースでは、「弱い外部」シンボルのようなものがあり、基本的には次のようになります。「私はこのシンボルの定義を提供していますが、if他の誰かも定義していますが、しないでください」エラーと見なしてください-この定義の代わりにその定義を使用してください。
すべてのシンボルのエントリを取得したら、リンカは「ピース」を配置してアドレスを割り当てる必要があります。ピースを配置する順序は多少異なります。通常、ピースのタイプに関するいくつかのフラグがあるため、(たとえば)すべての定数データが互いに隣り合って、すべてのコードのピースがお互いなど。私たちの単純なMS-DOSのようなシステムでは、これのほとんどはそれほど重要ではありません。
それが次のフェーズ、つまりローダーです。ローダーは通常、実行可能ファイルをロードするオペレーティングシステムの一部です。旧バージョン(CP/M、MS_DOS .comファイルなど)では、ローダーは実行可能ファイルからメモリにデータを読み取ってから、あるアドレスで実行を開始しました。少し最近のローダー(MS-DOS .exeファイルなど)では、 (多かれ少なかれ)同じ方法で開始します:ファイルをメモリに読み込みます。ただし、この場合、リンカによってそこに入力されたエントリに基づいて、実行可能ファイル内の絶対参照を「修正」して、上記の例では、スタートアップコードはアドレス1112でmain
を参照していますが、実行可能ファイルは(たとえば)4000のベースアドレスにロードされています。この場合、ローダーはそのアドレスを修正します。 5112を参照してください。ただし、この単純なシステムでは、ローダーは非常に単純なコードです。基本的には、再配置のリストを調べ、それぞれにベースアドレスを追加するだけです。
ここで、共有オブジェクトファイルやDLLなどをサポートするもう少し新しいOSについて考えてみましょう。これは基本的に、一部の作業をリンカーからローダーにシフトします。特に、.so/DLLで定義されているシンボルの場合、リンカーはnotがアドレス自体を割り当てようとします。
代わりに、基本的に「.so/DLLファイルXXXで定義されています」と書かれたシンボルテーブルエントリを作成します。リンカが実行可能ファイルを書き込むとき、これらのシンボルテーブルエントリのほとんどは基本的に、「シンボルXXXはファイルYYYで定義されています」と言って実行可能ファイルにコピーされます。その後、ローダーがファイルYYYとそのファイル内のシンボルXXXのアドレスを見つけ、実行可能ファイルで使用されている場所に正しいアドレスを入力します。リンカーと同様に、これは再帰的であるため、DLL AはDLL B内のシンボルを参照し、DLL Cなど。実行可能ファイルからすべての定義へのチェーンは長くなる可能性がありますが、プロセスの基本的な考え方はかなり単純です-外部参照のリストをスキャンして、それぞれの定義を見つけます。ほとんどの場合、多くのプロセス間で単一の実行可能ファイルを共有できるため、OSは通常、ロードされたモジュールのリストを持ち、すでにロードされているモジュールに到達した場合は、エントリを入力するだけです。そのため、最初から再読み込みするのではなく、完了します。
繰り返しますが、考慮すべき雑多な部分があります。たとえば、共有は通常、セクションごとにのみ行われ、ファイルごとには行われません。たとえば、ファイルにいくつかのコードといくつかの(一定でない)データがある場合、すべてのプロセスは同じコードセクションを共有しますが、それぞれが独自のデータのコピーを取得します。
リンカーの詳細については、コンパイラーと組み合わせて説明するのが一般的だと思います。これらは、さまざまなモジュールを1つのまとまりのあるユニットにまとめ、そのコード内でアドレスを確定するためのものです。一部は最適化を実行しようとするかもしれません。
ローダーについて詳しく知るために、ローダーをリンカーの同義語として意味しない限り、特定のアーキテクチャー向けのコンパイラーを作成することと組み合わせて、ローダーを一般的に説明すると思います。ローダーは、コンパイルされたソフトウェアを開いて実行する方法をオペレーティングシステムに指示する実行可能ファイルヘッダーの一部と考えています。
ウィキペディアの記事を読むことで、あなたが探しているよりも多くの情報が提供されることに同意します。それらがどこで開発に参加するかについては、通常、プロジェクトの制御を超えており、使用するオペレーティングシステムと開発パッケージの選択の一部です。 (たとえば)MSVCを使用することは非常にまれですが、GCCベースのリンカーを実行したいのですが...非標準のリンカーを使用した唯一の場所は、開発コピーを使用していたときのIBMでした。
これらのトピックについてより具体的で具体的な質問がある場合は、はるかに良い回答が見つかると思います。
リンカーとローダーは、関連しているが別々の2つの概念です。
リンカーはコンパイラ理論の一部です。複数のモジュール(ソースコードファイル)で構成されるプロジェクトをコンパイルする場合、コンパイラは、各ソースモジュールに対して単一の中間ファイルを出力するのが一般的です。これにはいくつかの利点があります。その1つは、1つのファイルに変更を加えてから再コンパイルする必要がある場合、ローカルで1つの変更を行っただけでプロジェクト全体を再ビルドする必要がないことです。
ただし、これは、あるモジュールに別のモジュールの関数を呼び出すコードがある場合、他の関数の場所がないため、コンパイラはCALL
命令を生成できないことを意味します。これは別の中間ファイルにあり、その中間のソースファイルをローカルで変更して再コンパイルすると、関数の正確な場所が変わる可能性があります。したがって、代わりに、「正確なアドレスがわからないこの関数が必要です」という「外部参照トークン」を挿入します(正確には、それが何であるか、またはどのように見えるかは重要ではありません。抽象的な概念として考えてください)。現時点では。"
すべてが中間ファイルにコンパイルされると、リンカーがジョブを終了します。すべての中間ファイルを調べ、それらをまとめて最終的なバイナリにリンクします。物事をまとめているので、すべての関数の実際のアドレスを知っているので、外部参照トークンを実際のCALL
命令に置き換えて、バイナリの正しい場所に置くことができます。
一方、ローダーはコンパイラではなくオペレーティングシステムに属します。その仕事は、バイナリをメモリにロードして実行できるようにすることと、リンカは既知のコードしか解決できないため、リンクプロセスを完了することです。プログラムがDLLを使用している場合、それらはコンパイル済みバイナリの外部にあるため、リンカーはそれらのアドレスを知りません。 OSのローダーが認識できる形式で外部バイナリトークンを最終的なバイナリに残し、すべてがメモリに読み込まれると、ローダーが通過してこれらのトークンをDLLの実際の関数アドレスと照合します。