484

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

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

4

5 に答える 5

646

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

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

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

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

前処理

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

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

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

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

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

コンパイル

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

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

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

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

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

リンキング

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

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

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

于 2011-06-07T11:05:06.887 に答える
25

標準前面:

  • 翻訳単位は、ソース ファイル、インクルード ヘッダー、およびソース ファイルから、条件付きインクルード プリプロセッサ ディレクティブによってスキップされたソース行を除いたものの組み合わせです。

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

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

于 2011-06-07T11:23:16.063 に答える
22

スキニーは、CPUがメモリアドレスからデータをロードし、データをメモリアドレスに保存し、メモリアドレスから順番に命令を実行し、処理される命令のシーケンスにいくつかの条件付きジャンプがあることです。これらの 3 つのカテゴリの命令にはそれぞれ、マシン命令で使用されるメモリ セルへのアドレスの計算が含まれます。機械語命令は関連する特定の命令に応じて可変長であり、機械語コードを作成する際にそれらの可変長をつなぎ合わせるため、アドレスの計算と作成には 2 段階のプロセスが必要です。

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

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

リンカーは、コードのブロックを個別にコンパイルできるようにするものです。これにより、コードを構築する全体的なプロセスが高速化され、後でブロックがどのように使用されるかについてある程度の柔軟性が得られます。つまり、ブロックをメモリ内で再配置することができます。

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

それがリンカーに到達する場所です。リンカはまず、これらすべてのマシン コード ブロックをエンドツーエンドでまとめ、それぞれの開始位置を書き留めます。次に、モジュール内の相対オフセットと、より大きなレイアウトでのモジュールの絶対位置を加算して、固定するアドレスを計算します。

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

于 2014-01-10T10:50:10.387 に答える