このページの内容 |
---|
このページの内容 |
ビルド=コンパイル×ソースファイル数+リンク |
コンパイル詳細:準備屋プリプロセッサ |
コンパイラ詳細:信じるコンパイラ |
リンカ詳細:全てをまとめる最後の重鎮 |
メイクファイル:ビルドの手順書 |
まとめ |
ありがたいスポンサー様 |
---|
さて、今まで 『ビルド』 という言葉を使っていましたが、このページでは 『ビルド』 とは具体的に何をしているのかについて迫ってみたいと思います。ビルドの仕組みを分かってないと、VC++を使うにも支障が出ますし、デバッグ効率も落ちることになってしまいます。さけて通れない道なので、ぜひこの機会に学習してください。
いつものように一番最初にまとめてみます。今回のまとめは下の図です。今回はこの図の内容を説明していきます。
ビルドの流れ
helloworldプロジェクトをビルドしたときの出力を再度掲載します。
------------構成: helloworld - Win32 Release------------ リソースをコンパイル中... コンパイル中... StdAfx.cpp コンパイル中... helloworld.cpp リンク中...helloworld.exe - エラー 0、警告 0
VC++はビルドするとき、まず最初にプロジェクト内の全てのソースファイルをコンパイルします。今回の場合は、プロジェクトに 『StdAfx.cpp』 と 『helloworld.cpp』 『helloworld.rc』 があったので、これらのソースファイルを1つずつコンパイルしてオブジェクトファイルを作成します。C/C++ のソースファイルの場合、1つのソースファイルと(ソースファイルでインクルードされた)ヘッダファイルから1つのオブジェクトファイルを作成します。オブジェクトファイルにはコンパイルの結果がバイナリ形式で納められています。
全てのソースファイルをコンパイルし終えると、オブジェクトファイルや各種ライブラリファイル(printfなどの関数の情報を含んでいる)を結合して実行可能ファイルを作成します。このことを、リンカがオブジェクトファイルをリンクすると言います。リンクに失敗するとリンクエラーになってしまい、ビルドに失敗します。
では、以下でそれぞれのプロセスをもうちょっと細かく見ていきましょう。
コンパイラは、まずプリプロセッサを呼び出します。プリプロセッサは、#defineや#include、#ifdef、#endifなどのプリプロセッサ文を解釈し、コンパイラへの入力を作成します。
例えば、次のようなソースファイルとヘッダファイルがあったとします。
// hoge.cpp #include <stdio.h> #include "hoge.h" void main(){ myprint() ; } void myprint(){ puts( MY_STRING) ; } // hoge.h #define MY_STRING "defined strings" // prototype void myprint() ; // end of hoge.h
これらのファイルを、『Win32 Console Application』のプロジェクトに入れてコンパイルしてみました。コンパイルのときに、オプションとして『/EP /C』 をつけるとプリプロセッサの出力をコメント付きで表示できます(コンパイラオプションの変更の仕方はそのうち書きますね^^;)。結果は、以下のようになりました。
// hoge.cpp /*** *stdio.h - definitions/declarations for standard I/O routines (中略) #pragma pack(pop) // hoge.h // prototype void myprint() ; // end of hoge.h void main(){ myprint() ; } void myprint(){ puts( "defined strings") ; }
上の出力から分かるように、#include 文は、指定したヘッダファイルをソースファイルの #include 文の場所にコピーします。実際に、#include <stdio.h>だった場所に、stdio.h が展開されているのが分かると思います。(stdio.hは、[Visual Studioのフォルダ]\VC98\Include にあるはずです)
同様に、#include "hoge.h"が展開されているのも確認できると思います。ただし、#define 文が解釈され、hoge.cpp のMY_STRINGが "定義された文字列" に変換されているのが確認できますね。
プリプロセッサの働きの雰囲気はつかめたでしょうか。プリプロセッサの出力を受け取るのがコンパイラです。コンパイラは、C++の言語仕様に基づき、1つのオブジェクトファイルを作成します。helloworld プロジェクトの helloworld.cpp からは helloworld.obj、StdAfx.cpp からは StdAfx.obj を作成します。
ところで、先ほどの、hoge.cppの例では、puts関数を使用していましたが、puts の内容は hoge.obj には含まれません。つまり、puts関数の内容が分からないくても、hoge.cpp をコンパイルできるのです。
これは、stdio.h の中で、puts関数がプロトタイプ宣言されているためです。putsのプロトタイプ宣言は次のような形をしています。
int puts( const char *string );
プロトタイプ宣言は、どこかにputsという関数の定義(実体)があることを宣言します。もしかしたら、hoge.cppの中にあるかもしれないし、ないかもしれない。今回はたまたま無かったのです。
コンパイラは、hoge.cppの中にputsがなかった場合、どこか別のオブジェクトファイルに定義があると信じて hoge.obj を作成します。その際、オブジェクトファイルには、「puts がどこか別のところにあると信じているよ」という情報を記録します。このような状態を、hoge.objはputs関数を 『外部参照』 している、といいます。
さて、プロジェクトのソースファイルを1つずつコンパイルしてオブジェクトファイルを生成できたら、次はリンカの出番です。コンパイルの結果に1つでもエラーがあってオブジェクトファイルを作れなかった場合はリンカは登場しません。コンパイルエラーを修正して再度ビルドしてください。
ではリンカの動作を説明していきます。以下のような2つのソースがあるプロジェクトを考えてみましょう。
// A.cpp #include <stdio.h> int a() ; int b() ; void main(){ int x = a() ; int y = b() ; printf( "%d,%d\n", x, y) ; } int a(){ return 3 ; } // B.cpp int a() ; int b(){ return a() + 2 ; }
下の図は、a.objの関数参照の状態を図示したものです。mainの中で呼び出されている関数のうち、a関数は a.obj の中に定義が存在します。そのため、この関数への参照は既に確定されています(黒色の矢印)。しかし、b関数とprintf関数に関しては、a.objの中に定義が存在しないため、外部参照しています(赤色の矢印)。
a.obj の関数参照イメージ
B.cppでは、b()が定義されていて、B.cpp に定義がないa()を外部参照しているので、次のような感じになると思います。
b.obj の関数参照イメージ
通常のアプリケーションの場合、リンカはコンパイラが作成したオブジェクトファイル(この場合は a.obj と b.obj)の他に、以下のライブラリファイルもリンクします。
kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib libc.lib
これらのライブラリファイルは Microsoft が用意したもので、色々なライブラリ関数の定義(実体)が記録されています。例えば、libc.lib には printf や puts などのランタイムライブラリの実体があります。ライブラリファイルはオブジェクトファイルと同じフォーマットで記録されているため、オブジェクトファイルと同じと考えて問題ありません。(実は違うものもあるのですが細かくなるのでここでは触れません。ここに詳しい情報があります)
よって、ライブラリファイルのイメージは下図のような感じです。libc.lib 内のprintf関数は外部参照を持たないという意味で下のような図にしました。printf関数が libc.lib 内の他の関数を参照しているかもしれませんが、Microsoft の実装方法が分からないのでその部分は図には記していません。
libc.lib の関数参照イメージ
前置きが長くなりました。リンカは、これらのオブジェクトファイルとライブラリファイルを結合します。結合後のイメージはこんな感じです。
.OBJと.LIBを結合
しかし、これでは実行可能ファイルとして不完全です。例えばmain()関数の中では、a()のアドレスは分かっているのでint a = a();は実行できますが、b()関数の場所は分からないままになっているのでint y = b();を実行できません。
そのため、リンカは結合したあと、それぞれの 『外部参照』 に対して、それらが実際に定義されている場所を探します。例えば、元 a.obj にあったb()関数への外部参照は、元 b.obj の中に定義を見つけることができます。このことを、『外部参照は解決できた』 と言います。下の図はそのときのイメージ図です。
main()の中のb()への外部参照が解決
同様に、全ての外部参照を解決すると、以下のようになります。
すべての外部参照を解決
このように、リンカの仕事は、『オブジェクトファイルの結合+外部参照の解決』 です。次のような場合は、リンクエラーになり、ビルドは中断されます。
リンクが終了すると、リンカは実行可能ファイルを出力します。リンカが出力できるのは、.EXE ファイルや .DLL ファイルです。
さて、最後にメイクファイルについて解説します。メイクファイルには、「○○.cppというソースファイルをコンパイルして、○○.objを作成する」 とか 「○○.obj と ××.obj と これこれのlibファイルをリンクする」といったビルドするときの手順が細かく記されています。いわば、ビルドの手順書といったところです。
しかし、VC++を使ってビルドする場合は、メイクファイルを作成する必要はありません。これは、VC++がメイクファイルを自動的に作成しているためだと考えてください。コマンドラインからビルドする昔ながらの技術屋さんははメイクファイルが必要ですが、GUIでビルドする場合は必要ありません。
にもかかわらず、次のようにいう人もいます。
プログラマたるもの、一度はメイクファイルを自分で書け!」
これは、メイクファイル書いて試行錯誤しているうちに、ビルドの詳細を理解できるからです。ただ、ビルドの詳細はこのページで述べちゃったので、メイクファイルの書き方は覚えなくていいです(笑)
おつかれさまです。今回の内容はかなりボリュームがあったと思います。書く方も疲れちゃいました(^^;
さて、これでページの最初にあった図も理解できると思います。ただ、プリプロセッサがメイクファイルを入力として受け取っているように見えますが、実際はそうではないので注意してください。
再掲:ビルドの流れ