| 139 | |
| 140 | === コンパイル言語 === |
| 141 | |
| 142 | C 言語はコンパイル言語です。 C コンパイラが C 言語で書かれたプログラムを実際にコンピュータ上で動作するプログラム (いわゆる「実行ファイル」と呼ばれるもの) に変換するまでの伝統的な手順は概ね以下の通りです。 |
| 143 | |
| 144 | 1. '''プリプロセッサ'''により、番号記号 "#" で始まる行を処理する (ファイル挿入や行削除など)。 |
| 145 | 1. '''コンパイラ'''により、 C 言語で書かれたプログラムを、アセンブリ言語のプログラムに変換する。 |
| 146 | 1. '''アセンブラ'''により、アセンブリ言語のプログラムを機械語のバイナリファイルに変換する。 |
| 147 | 1. '''リンカ'''により、関数や変数などの名前を頼りにバイナリファイルをつなぎ合わせ、実行可能なプログラム (実行ファイル) に変換する。 |
| 148 | |
| 149 | この動作を知っていることは重要です。プリプロセッサの役割を理解していることは、マクロなどのプリプロセッサ行をどう活用すべきかを考える指針になりますし、コンパイラがアセンブリ言語のプログラムを生成できることを知っていれば、動作効率を最適化するための参考になるでしょうし、コンパイルとリンクが処理の段階としては別個であることを理解していれば、リンクエラーを解決する手立ての見当がつくようになります。 |
| 150 | |
| 151 | 1 の'''プリプロセッサ'''は、プログラム中の番号記号 "#" で始まる行 (プリプロセッサ行) を「先に」処理しておくツールです。先ほどのサンプルプログラムで言うと、 #include で始まる行や、 #define で始まる行がそれに該当します。プリプロセッサが #include で始まる行を見つけると、そこに書かれているファイル名のファイルを探してきて、その行の位置に挿入します。また、 #define で始まる行を見つけると、その直後に書かれた名前と同じ単語を、その更に後ろに書かれた内容で置換します (例えば、 "MAX_CALC_NUM" と書かれた箇所が、事前に "1000" に書き換えられます)。 |
| 152 | |
| 153 | 2 の'''コンパイラ'''は、C で書かれたプログラムを、あくまでアセンブリ言語のプログラムに置き換えるだけです。実は、 C 言語はアセンブリ言語に割と置き換えやすいように設計されています。但し、ただ単に置き換えるよりも、工夫して置き換えた方が、動作効率が良くなったり、実行ファイルを小さくまとめられたりしそうな箇所については、コンパイラが独自に判断して、上手く変換してくれる場合もあります。使用するコンパイラや、コンパイル時に指定するオプションによって、プログラムの実行速度や、実行ファイルのファイルサイズなどが変わる場合があるのは、その為です。 |
| 154 | |
| 155 | GCC でも、 -S オプションを指定することによって、アセンブリ言語で書かれたプログラムを生成することができます。以下のように実行すると、アセンブリ言語によるプログラムファイル test.s が生成されます。 |
| 156 | |
| 157 | {{{ |
| 158 | $ gcc -S test.c |
| 159 | }}} |
| 160 | |
| 161 | アセンブリ言語とは、機械語と呼ばれる、コンピュータが直接命令として理解できる数値の羅列を、その数値毎に名前をつけて、命令毎に行に起こしたものです。昔は機械語を直接コンピュータに入力していましたが、数値の羅列ではさすがにわかりにくすぎるので、命令毎に書き起こして分かりやすくしたのがアセンブリ言語でした。 |
| 162 | |
| 163 | しかし、このアプローチはお世辞にも直感的とは言えません。アセンブリ言語には変数の概念が無く、値を書き込むメモリーの位置や、どのレジスタ[[FootNote(CPU などのプロセッサ (演算処理装置) が命令を処理する際、その命令にどの値を用いるかは命令の種類によって決まっていたり、あるいは命令と一緒に指定して決めたりします。その、命令を処理する際に用いる値を格納する場所がレジスタです。これに対し、メインメモリーは、あとでレジスタに取り込んで命令に用いるつもりで用意した値を保存しておく場所であり、メモリー上に記録されている値を直接命令によって処理できるわけではありません。)]]を用いるかなどは、プログラマーが自分で管理しなければなりません。また、高級言語では 1行の計算式で書き表せる演算を、アセンブリ言語では演算子毎に (演算順序を気にしながら!) 行を分けて書き連ねる必要があり、感覚的には非常に冗長です。 |
| 164 | |
| 165 | 例えば、以下の非常に簡単なプログラム |
| 166 | |
| 167 | {{{ |
| 168 | #include <stdio.h> |
| 169 | |
| 170 | int main() |
| 171 | { |
| 172 | int a[] = { 1, 2, 3, 4 }; |
| 173 | int b = 0; |
| 174 | |
| 175 | b = a[0] + a[1] * a[2] - a[3]; |
| 176 | |
| 177 | printf("a = %d\n", b); |
| 178 | |
| 179 | return 0; |
| 180 | } |
| 181 | }}} |
| 182 | |
| 183 | のうち、以下の行 |
| 184 | |
| 185 | {{{ |
| 186 | b = a[0] + a[1] * a[2] - a[3]; |
| 187 | }}} |
| 188 | |
| 189 | は、アセンブリ言語に変換すると、以下のようになります[[FootNote(変換したアセンブリソースの抜粋です。なお、変換に用いた環境は、 Intel CPU を搭載した一般的な Windows XP パソコン上にインストールされた MinGW の GCC 4.5.0 です。)]]。 |
| 190 | |
| 191 | {{{ |
| 192 | movl 28(%esp), %edx |
| 193 | movl 32(%esp), %ecx |
| 194 | movl 36(%esp), %eax |
| 195 | imull %ecx, %eax |
| 196 | addl %eax, %edx |
| 197 | movl 40(%esp), %eax |
| 198 | movl %edx, %ecx |
| 199 | subl %eax, %ecx |
| 200 | }}} |
| 201 | |
| 202 | メモリー上に記録されている値をレジスタにコピーし、掛け算して、足し算して、更にメモリーからレジスタにコピーして、引き算する、といった処理内容です。この例の場合、 imull という 1ステップで掛け算を処理してくれる便利な命令があるのでまだ分かりやすいですが、もしもこの命令が搭載されていないコンピュータだった場合、足し算を複数回繰り返すループとして記述されていたでしょう。 |