AC ソース ファイルは、(1) プリプロセッサ ディレクティブを検索してそれらのアクションを実行するプリプロセッサ ユーティリティによって C ソース コードが処理されるプリプロセッサ ステージと、(2) 処理された C ソース コードが次に処理されるコンパイル ステージの 2 つの主要なステージを通過します。オブジェクト コード ファイルを生成するために実際にコンパイルされます。
プリプロセッサは、テキスト操作を行うユーティリティです。プリプロセッサ ディレクティブを含む可能性のあるテキスト (通常は C ソース コード) を含むファイルを入力として受け取り、見つかったディレクティブをテキスト入力に適用してテキスト出力を生成することにより、ファイルの修正バージョンを出力します。
プリプロセッサがテキスト操作を行っているため、ファイルは C ソース コードである必要はありません。make
私は、プリプロセッサ ディレクティブをメイク ファイルに含めることができるようにすることで、ユーティリティを拡張するために使用される C プリプロセッサを見てきました。C プリプロセッサ ディレクティブを含む make ファイルは、C プリプロセッサ ユーティリティを介して実行され、結果の出力がフィードされmake
て、make ターゲットの実際のビルドが実行されます。
ライブラリとリンク
ライブラリとは、さまざまな機能のオブジェクト コードを含むファイルです。複数のソース ファイルを 1 つのファイルにコンパイルするときに、それらの出力をパッケージ化する方法です。多くの場合、ライブラリ ファイルはヘッダー ファイル (インクルード ファイル) と共に提供され、通常は .h ファイル拡張子が付いています。ヘッダー ファイルには、関数宣言、グローバル変数宣言、およびライブラリに必要なプリプロセッサ ディレクティブが含まれています。したがって、ライブラリを使用するには、#include
ディレクティブを使用して提供されるヘッダー ファイルをインクルードし、ライブラリ ファイルとリンクします。
ライブラリ ファイルの優れた機能は、ソース コード自体ではなく、ソース コードのコンパイル済みバージョンを提供することです。一方、ライブラリ ファイルにはコンパイル済みのソース コードが含まれているため、ライブラリ ファイルの生成に使用されるコンパイラは、独自のソース コード ファイルのコンパイルに使用されるコンパイラと互換性がある必要があります。
一般的に使用されるライブラリには 2 つのタイプがあります。最初の古いタイプはスタティック ライブラリです。2 つ目以降はダイナミック ライブラリ (Windows ではダイナミック リンク ライブラリまたは DLL、Linux では共有ライブラリまたは SO) です。2 つの違いは、ライブラリ内の関数が、ライブラリ ファイルを使用している実行可能ファイルにバインドされている場合です。
リンカーは、さまざまなオブジェクト ファイルとライブラリ ファイルを使用して実行可能ファイルを作成するユーティリティです。外部関数またはグローバル関数または変数が C ソース ファイルで使用される場合、その時点で関数または変数のアドレスを挿入する必要があることをリンカーに伝えるために、一種のマーカーが使用されます。
C コンパイラは、コンパイルするソースの内容のみを認識しており、オブジェクト ファイルやライブラリなどの他のファイルの内容は認識していません。したがって、リンカーの仕事は、さまざまなオブジェクト ファイルとライブラリを取得し、マーカーを実際の接続に置き換えることで、パーツ間の最終的な接続を作成することです。したがって、リンカーはさまざまなコンポーネントを「リンク」するユーティリティであり、オブジェクト ファイルとライブラリ内のグローバル関数または変数のマーカーを、そのグローバル関数または変数用に生成された実際のオブジェクト コードへのリンクに置き換えます。
リンカ段階では、静的ライブラリと動的ライブラリまたは共有ライブラリの違いが明らかになります。スタティック ライブラリを使用すると、ライブラリの実際のオブジェクト コードがアプリケーションの実行可能ファイルに含まれます。動的ライブラリまたは共有ライブラリを使用する場合、アプリケーションの実行可能ファイルに含まれるオブジェクト コードは、アプリケーションの実行時に共有ライブラリを見つけて接続するためのコードです。
場合によっては、同じグローバル関数名が複数の異なるオブジェクト ファイルまたはライブラリで使用されることがあるため、リンカは通常、最初に検出したものだけを使用し、見つかった他のものについては警告を発します。
コンパイルとリンクのまとめ
したがって、C プログラムのコンパイルとリンクの基本的なプロセスは次のとおりです。
プリプロセッサ ユーティリティは、コンパイルする C ソースを生成します
コンパイラは、C ソースをオブジェクト コードにコンパイルし、一連のオブジェクト ファイルを生成します。
リンカーは、さまざまなオブジェクト ファイルとライブラリを実行可能ファイルにリンクします。
上記は基本的なプロセスですが、動的ライブラリを使用する場合、特に生成されるアプリケーションの一部に生成中の動的ライブラリがある場合は、さらに複雑になる可能性があります。
ローダー
アプリケーションが実際にメモリにロードされ、実行が開始される段階もあります。オペレーティング システムは、アプリケーションの実行可能ファイルを読み取ってメモリにロードし、アプリケーションの実行を開始するローダーというユーティリティを提供します。実行可能ファイルの開始点またはエントリ ポイントは実行可能ファイルで指定されるため、ローダーが実行可能ファイルをメモリに読み取った後、エントリ ポイントのメモリ アドレスにジャンプして実行可能ファイルの実行を開始します。
リンカが遭遇する可能性のある問題の 1 つは、実際のメモリ アドレスを必要とするオブジェクト コード ファイルを処理しているときに、マーカーに遭遇する場合があることです。ただし、アドレスはアプリケーションがメモリ内のどこにロードされているかによって異なるため、リンカは実際のメモリ アドレスを知りません。そのため、リンカーは、ローダーが実行可能ファイルをメモリにロードし、実行を開始する準備をしているときに、ローダー ユーティリティが修正するものとしてマークします。
仮想アドレスから物理アドレスへのマッピングまたは変換をサポートするハードウェアを備えた最新の CPU では、この実際のメモリ アドレスの問題が問題になることはめったにありません。各アプリケーションは同じ仮想アドレスにロードされ、ハードウェア アドレス変換によって実際の物理アドレスが処理されます。ただし、アドレス変換用のメモリ管理ユニット (MMU) ハードウェア サポートがない古い CPU やマイクロコントローラーなどの低コストの CPU では、この問題に対処する必要があります。
エントリ ポイントと C ランタイム
最後のトピックは、C ランタイムとmain()
実行可能なエントリ ポイントです。
C ランタイムは、C で記述されたアプリケーションのエントリ ポイントを含む、コンパイラの製造元によって提供されるオブジェクト コードです。main()
関数は、アプリケーションを記述するプログラマによって提供されるエントリ ポイントですが、これはローダーが認識するエントリ ポイントではありません。このmain()
関数は、アプリケーションの開始後に C ランタイムによって呼び出され、C ランタイム コードによってアプリケーションの環境が設定されます。
C ランタイムは標準 C ライブラリではありません。C ランタイムの目的は、アプリケーションのランタイム環境を管理することです。標準 C ライブラリの目的は、一連の便利なユーティリティ関数を提供して、プログラマが独自に作成する必要がないようにすることです。
ローダーがアプリケーションをロードし、C ランタイムによって提供されるエントリ ポイントにジャンプすると、C ランタイムは、アプリケーションに適切なランタイム環境を提供するために必要なさまざまな初期化アクションを実行します。これが完了すると、C ランタイムはmain()
関数を呼び出して、アプリケーション開発者またはプログラマーが作成したコードの実行を開始します。main()
が戻ったとき、またはexit()
関数が呼び出されたときに、C ランタイムは、アプリケーションをクリーンアップして閉じるために必要なアクションを実行します。