コンパイル/リンクプロセスはどのように機能しますか?


416

コンパイルとリンクのプロセスはどのように機能しますか?

(注:これはStack OverflowのC ++ FAQへのエントリになることを目的としています。このフォームでFAQを提供するという考えを批評したい場合は、これをすべて開始したメタへの投稿がそのための場所になります。回答その質問はC ++チャットルームで監視され、FAQのアイデアが最初から始まっているため、アイデアを思いついた人があなたの答えを読む可能性が非常に高くなります。

回答:


554

C ++プログラムのコンパイルには、次の3つのステップが含まれます。

  1. 前処理:プリプロセッサはC ++ソースコードファイルを受け取り、#includes、#defines、およびその他のプリプロセッサディレクティブを処理します。このステップの出力は、プリプロセッサディレクティブのない「純粋な」C ++ファイルです。

  2. コンパイル:コンパイラはプリプロセッサの出力を受け取り、そこからオブジェクトファイルを生成します。

  3. リンク:リンカは、コンパイラによって生成されたオブジェクトファイルを受け取り、ライブラリまたは実行可能ファイルを生成します。

前処理

プリプロセッサは、プリプロセッサディレクティブなど#include処理#defineます。C ++の構文に依存しないため、注意して使用する必要があります。

#includeディレクティブをそれぞれのファイルの内容(通常は単なる宣言)で置き換え、マクロ(#define)の置き換えを行い#if#ifdefおよび#ifndefディレクティブに応じてテキストの異なる部分を選択することにより、一度に1つのC ++ソースファイルで機能します。

プリプロセッサーは、前処理トークンのストリームを処理します。マクロ置換は、トークンを他のトークンに置き換えることとして定義され##ます(演算子は、意味のある場合に2つのトークンをマージできるようにします)。

このすべての後、プリプロセッサは、上記の変換の結果として得られるトークンのストリームである単一の出力を生成します。また、各行がどこから来たかをコンパイラーに伝える特別なマーカーをいくつか追加して、それらを使用して適切なエラーメッセージを生成できるようにします。

#ifand #errorディレクティブを巧みに使用すると、この段階でいくつかのエラーが発生する可能性があります。

コンパイル

コンパイル手順は、プリプロセッサの各出力で実行されます。コンパイラーは純粋なC ++ソースコード(現在はプリプロセッサーディレクティブなし)を解析し、アセンブリコードに変換します。次に、そのコードをマシンコードにアセンブルする基本的なバックエンド(ツールチェーンのアセンブラー)を呼び出して、何らかの形式(ELF、COFF、a.out、...)で実際のバイナリファイルを生成します。このオブジェクトファイルには、入力で定義されたシンボルのコンパイルされたコード(バイナリ形式)が含まれています。オブジェクトファイル内のシンボルは、名前で参照されます。

オブジェクトファイルは、定義されていないシンボルを参照できます。これは、宣言を使用し、その定義を提供しない場合に当てはまります。コンパイラはこれを気にせず、ソースコードが整形式である限り、オブジェクトファイルを喜んで生成します。

コンパイラでは通常、この時点でコンパイルを停止できます。これにより、各ソースコードファイルを個別にコンパイルできるため、非常に便利です。これが提供する利点は、単一のファイルを変更するだけですべてを再コンパイルする必要がないことです。

生成されたオブジェクトファイルは、後で簡単に再利用できるように、スタティックライブラリと呼ばれる特別なアーカイブに置くことができます。

構文エラーや失敗したオーバーロード解決エラーなどの「通常の」コンパイラエラーが報告されるのはこの段階です。

リンク

リンカは、コンパイラが生成したオブジェクトファイルから最終的なコンパイル出力を生成するものです。この出力は、共有(または動的)ライブラリ(名前は似ていますが、前述の静的ライブラリとあまり共通点がありません)または実行可能ファイルのいずれかです。

未定義のシンボルへの参照を正しいアドレスに置き換えることにより、すべてのオブジェクトファイルをリンクします。これらの各シンボルは、他のオブジェクトファイルまたはライブラリで定義できます。それらが標準ライブラリ以外のライブラリで定義されている場合は、それらをリンカに通知する必要があります。

この段階で最も一般的なエラーは、定義の欠落または重複した定義です。前者は、定義が存在しない(つまり、定義が記述されていない)か、定義が存在するオブジェクトファイルまたはライブラリがリンカーに提供されなかったことを意味します。後者は明白です。同じシンボルが2つの異なるオブジェクトファイルまたはライブラリで定義されています。


39
コンパイル段階では、オブジェクトファイルに変換する前にアセンブラも呼び出します。
manav mn 2013

3
最適化はどこに適用されますか?一見すると、コンパイルのステップで行われるように見えますが、一方で、適切な最適化はリンクの後でしか実行できないと想像できます。
Bart van Heukelom、2014年

6
@BartvanHeukelomは伝統的にコンパイル中に行われていましたが、最近のコンパイラはいわゆる「リンク時最適化」をサポートしており、翻訳単位全体で最適化できるという利点があります。
R.マルティーニョフェルナンデス

3
Cにも同じ手順がありますか?
Kevin Zhu 14

6
リンカがライブラリのクラス/メソッドを参照するシンボルをアドレスに変換する場合、それは、ライブラリバイナリがOSが一定に保つメモリアドレスに格納されることを意味しますか?リンカが、たとえば、すべてのターゲットシステムのstdioバイナリの正確なアドレスをどのようにして知るかについて、私は混乱しています。ファイルパスは常に同じですが、正確なアドレスは変わる可能性がありますよね?
ダンカーター

42

:このトピックでは、CProgramming.comで議論されている
https://www.cprogramming.com/compilingandlinking.html

ここに著者が書いたものがあります:

コンパイルは、実行可能ファイルを作成することとはまったく異なります。代わりに、実行可能ファイルの作成は、コンパイルとリンクの2つのコンポーネントに分かれた多段階のプロセスです。実際には、プログラムが「正常にコンパイル」されたとしても、リンク段階のエラーのために実際には機能しない可能性があります。ソースコードファイルから実行可能ファイルに移行するプロセス全体を、ビルドと呼ぶ方がよいでしょう。

コンパイル

コンパイルとは、ソースコードファイル(.c、.cc、または.cpp)の処理と「オブジェクト」ファイルの作成を指します。この手順では、ユーザーが実際に実行できるものは何も作成されません。代わりに、コンパイラは、コンパイルされたソースコードファイルに対応する機械語命令を生成するだけです。たとえば、3つの個別のファイルをコンパイルする(ただしリンクしない)場合、3つのオブジェクトファイルが出力として作成され、それぞれに.oまたは.objという名前が付けられます(拡張子はコンパイラによって異なります)。これらの各ファイルには、ソースコードファイルから機械語ファイルへの翻訳が含まれていますが、まだ実行できません。それらを、オペレーティングシステムが使用できる実行可能ファイルに変換する必要があります。ここでリンカが登場します。

リンク

リンクとは、複数のオブジェクトファイルから単一の実行可能ファイルを作成することを指します。このステップでは、リンカが未定義の関数(通常、メイン自体)について不平を言うことがよくあります。コンパイル中に、コンパイラーが特定の関数の定義を見つけられなかった場合、その関数は別のファイルで定義されていると見なされます。これが当てはまらない場合、コンパイラーが知る方法はありません-一度に複数のファイルの内容を調べません。一方、リンカは複数のファイルを調べて、言及されていない関数の参照を見つけようとする場合があります。

コンパイルとリンクのステップが別々になっている理由を尋ねるかもしれません。まず、その方法で実装する方がおそらく簡単です。コンパイラーが機能し、リンカーが機能します。関数を分離しておくことにより、プログラムの複雑さが軽減されます。別の(より明白な)利点は、これにより、ファイルが変更されるたびにコンパイル手順をやり直す必要なく、大きなプログラムを作成できることです。代わりに、いわゆる「条件付きコンパイル」を使用して、変更されたソースファイルのみをコンパイルする必要があります。残りの部分では、オブジェクトファイルはリンカにとって十分な入力です。最後に、これにより、プリコンパイル済みコードのライブラリの実装が簡単になります。オブジェクトファイルを作成し、他のオブジェクトファイルと同じようにリンクするだけです。

条件のコンパイルの利点を最大限に活用するには、最後にコンパイルしてから変更したファイルを覚えておくよりも、プログラムを手助けする方が簡単でしょう。(もちろん、対応するオブジェクトファイルのタイムスタンプよりもタイムスタンプが大きいすべてのファイルを再コンパイルすることもできます。)統合開発環境(IDE)で作業している場合は、すでにこの処理が行われている可能性があります。コマンドラインツールを使用している場合、ほとんどの* nixディストリビューションに付属しているmakeと呼ばれる気の利いたユーティリティがあります。条件付きコンパイルに加えて、プログラムのさまざまなコンパイルを許可するなど、他のいくつかの優れたプログラミング機能があります。たとえば、デバッグ用に詳細な出力を生成するバージョンがある場合などです。

コンパイルフェーズとリンクフェーズの違いを知っていると、バグを簡単に探すことができます。コンパイラー・エラーは通常、本質的に構文です-セミコロンがない、余分な括弧があります。リンクエラーは通常、定義の欠落または複数に関係しています。関数または変数がリンカから複数回定義されているというエラーを受け取った場合、それは、エラーが2つのソースコードファイルに同じ関数または変数があることを示しています。


1
私が理解していないことは、プリプロセッサが#includesなどを管理して1つのスーパーファイルを作成する場合、その後にリンクするものは何もないということです。
binarysmacker 2013

@binarysmacer以下に私が書いたものがあなたにとって意味のあるものかどうかを確認してください。私は問題を徹底的に説明しようとしました。
2014

3
@binarysmackerこれについてコメントするのは遅すぎますが、他の人がこれを役に立つと思うかもしれません。youtu.be/D0TazQIkc8Q基本的にヘッダーファイルをインクルードしますが、これらのヘッダーファイルには通常、変数/関数の宣言のみが含まれ、定義は含まれません。定義は別のソースファイルに存在する可能性があります。リンカーが役立ちます。変数/関数を使用するソースファイルとそれらを定義するソースファイルをリンクします。
Karan Joisher 2016年

24

標準的な正面では:

  • 変換ユニットは、ソースファイル、含まれるヘッダとソースファイル以下の条件付き包含プリプロセッサディレクティブによってスキップされ、任意のソース線の組み合わせです。

  • 標準では、翻訳の9つのフェーズが定義されています。最初の4つは前処理に対応し、次の3つはコンパイル、次の1つはテンプレートのインスタンス化(インスタンス化ユニットの生成)、最後の1つはリンクです。

実際には、8番目のフェーズ(テンプレートのインスタンス化)はコンパイルプロセス中に実行されることがよくありますが、一部のコンパイラはリンクフェーズにそれを遅らせ、一部は2つのフェーズに広げます。


14
9つのフェーズすべてをリストアップできますか?それは答えへの素晴らしい追加になると思います。:)
jalf


@ jalf、@ sbiが指す回答の最後のフェーズの直前にテンプレートのインスタンス化を追加します。IIRCワイド文字の処理の正確な表現には微妙な違いがありますが、ダイアグラムのラベルに表示されるとは思いません。
AProgrammer

2
@sbiええ、でもこれはFAQの質問になるはずですよね。それで、この情報はここで利用できませんか?;)
2011年

3
@AProgrammmer:それらを名前でリストするだけで役に立ちます。そうすれば、人々は、より詳細な情報が必要な場合に何を検索すればよいかを知っています。とにかく、どんな場合でもあなたの答えを+1しました:)
jalf

14

細いのは、CPUがメモリアドレスからデータをロードし、データをメモリアドレスに格納し、メモリアドレスから順番に命令を実行し、処理された一連の命令の条件付きジャンプを行うことです。これらの3つのカテゴリの各命令には、機械命令で使用されるメモリセルへのアドレスの計算が含まれます。機械語命令は、含まれる特定の命令に応じて可変長であり、機械語コードを構築するときにそれらの可変長をつなぎ合わせるため、アドレスの計算と構築には2つのステップのプロセスが必要です。

最初に、各セルで正確に何が行われるかを知る前に、できる限りメモリの割り当てをレイアウトします。命令、リテラル、およびデータを構成するバイト、単語、またはその他を把握します。メモリの割り当てと、プログラムを作成するための値の構築を開始するだけで、戻ってアドレスを修正する必要がある場所を書き留めます。その場所にダミーを配置して場所を埋めるだけで、メモリサイズの計算を続行できます。たとえば、最初のマシンコードは1つのセルをとります。次のマシンコードは、1つのマシンコードセルと2つのアドレスセルを含む3つのセルを必要とする場合があります。これで、アドレスポインターは4になりました。マシンセルに何が入るかはわかりますが、これはopコードですが、データが配置される場所がわかるまで、アドレスセルに何が入るかを計算するのを待つ必要があります。

ソースファイルが1つしかない場合、コンパイラーは理論的にはリンカーなしで完全に実行可能なマシンコードを生成できます。2パスプロセスでは、マシンのロードまたはストア命令によって参照されるすべてのデータセルへの実際のアドレスをすべて計算できます。そして、絶対ジャンプ命令によって参照される絶対アドレスのすべてを計算できます。これは、リンカーのないForthのコンパイラーのように単純なコンパイラーです。

リンカは、コードのブロックを個別にコンパイルできるものです。これにより、コードの構築プロセス全体がスピードアップし、ブロックを後で使用する方法にある程度の柔軟性が得られます。つまり、ブロックをメモリに再配置することができます。たとえば、すべてのアドレスに1000を追加して、ブロックを1000アドレスセルでスクートします。

したがって、コンパイラが出力するのは、まだ完全にビルドされていないラフなマシンコードですが、すべてのサイズがわかるようにレイアウトされています。つまり、すべての絶対アドレスが配置される場所の計算を開始できます。コンパイラーは、名前/アドレスのペアであるシンボルのリストも出力します。シンボルは、モジュール内のマシンコード内のメモリオフセットと名前を関連付けます。オフセットは、モジュール内のシンボルのメモリ位置への絶対距離です。

ここで、リンカにアクセスします。リンカは最初にこれらのマシンコードのブロックすべてを端から端までスラップし、各ブロックの開始位置を書き留めます。次に、モジュール内の相対オフセットと大きなレイアウトでのモジュールの絶対位置を加算して、修正するアドレスを計算します。

明らかに私はこれを単純化しすぎたので、それを把握できるようにしました。私には、混乱の一部であるオブジェクトファイルやシンボルテーブルなどの専門用語を意図的に使用していません。


13

GCCは、C / C ++プログラムを4つのステップで実行可能ファイルにコンパイルします。

たとえば、gcc -o hello hello.c次のように実行されます。

1.前処理

cpp.exeヘッダー(#include)を含み、マクロ(#define)を展開するGNU Cプリプロセッサー()による前処理。

cpp hello.c > hello.i

結果の中間ファイル「hello.i」には、拡張されたソースコードが含まれています。

2.コンパイル

コンパイラは、前処理されたソースコードを特定のプロセッサ用のアセンブリコードにコンパイルします。

gcc -S hello.i

-Sオプションは、オブジェクトコードの代わりにアセンブリコードを生成することを指定します。結果のアセンブリファイルは "hello.s"です。

3.組み立て

アセンブラ(as.exe)は、アセンブリコードをオブジェクトファイル "hello.o"のマシンコードに変換します。

as -o hello.o hello.s

4.リンカー

最後に、リンカ(ld.exe)はオブジェクトコードをライブラリコードにリンクして、実行可能ファイル「hello」を生成します。

    ld -o hello hello.o ...ライブラリ...

9

次のURLをご覧ください
。http ://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.htmlこのURLでは、C ++の完全なコンパイルプロセスが明確に紹介されています。


2
それを共有してくれてありがとう、理解するのはとても簡単で簡単です。
マーク

良いリソースです。ここにプロセスの基本的な説明を付けてください。答えは、アルゴリズムによって低品質のb / cとしてフラグが付けられ、URLだけです。
JasonB 2018

私が見つけた素晴らしい短いチュートリアル:calleerlandsson.com/the-four-stages-of-compiling-ac-program
Guy Avraham
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.