gccやclangなどのコンパイラツールセットは、多くの人が利用したことがあると思います。
とはいえ、コンパイラについてきちんと理解しようとすると、プリプロセッサ、アセンブラ、リンカ、オブジェクトファイル、実行ファイル、ELF など多くの用語が登場し、一筋縄ではいかないと感じることでしょう。
今回は、コンパイラの処理の流れを徹底的に解説していきます。目指すべくは、Cコンパイラを自作することができるレベルまで到達しましょう。
まず、様々なプログラミング言語やそれに紐づいて、多くのコンパイラが登場しているのですが、今回はgccを例にとって解説をしていきます。
コンパイラの処理の流れの全体像
本記事の最初に掲載した流れを紹介します。コンパイラの全体像は、次のようになっています。
ちなみに、コンパイラはPythonやRubyであっても内部的に利用されていますし、GoやRust、C言語でも異なっており、すべて同じような動きをしているわけではないことに注意してください。
今回は、C言語のコンパイラgccを例に、コンパイラを実行していきます。
今回は、自分の書いたソースコードが、プリプロセッサ、コンパイラ、アセンブラ、リンカ・ローダーによって処理されていきます。
実行ファイルは、ターゲットアーキテクチャ(x86, ARM, MIPS, RISC-V)ごとに規定された、バイナリ形式の実行可能なファイルです。これをOSに渡してあげるだけで、良い感じにCPUが実行してくれるものになります。
では、なぜ、ソースコードを直接実行可能なファイルにしないのか?という疑問も生じるかもしれませんが、ソースコードをアセンブリファイル、オブジェクトファイル、実行ファイルに分割することにはそれぞれ意味があるので、今回はその意味がわかるように解説をしていきます。
コンパイルがそれぞれ分割されている意味
コンパイラの過程が独立している意味としては、分割コンパイルが大きな理由としてあるでしょう。
分割コンパイルとは、ソースコードやライブラリを全部まとめて1つのアセンブラを作るのではなく、ライブラリやモジュール単位で分割して、アセンブラや実行ファイルを作ることです。
コンパイラはそれなりにメモリやCPUのリソースを消費しますし、実行時間も非常に多くかかるので、基本的には、変更していないものはそのまま再利用したいモチベーションがあります。そのため、基本的には、分割コンパイルすることになります。
一方で、分割してコンパイルする場合、分割したものをつなぐには、きちんと定められたインターフェースが必要になります。それが、ELFフォーマットなどのフォーマットになります。
コンパイルの一連の流れで登場するファイル
ソースコード
今回はC言語を例にしているので、ソースコードはプログラマが実際に実装するファイルに対応しています。PythonやRubyは、スクリプト型言語ですが、これらのソースコード
アセンブリファイル
アセンブリファイルは、人間がわかりやすいような形式で記載された、機械語に近い形式のファイルです。
プリプロセッサ
プリプロセッサは、コンパイルの一連の流れの中で、前処理を担うモジュールです。前処理をするだけなので、プリプロセッサ自体は、そこまで重要度が高いわけではありません。
具体的に行う前処理として、コメント行の削除やマクロコマンドの展開等をおこなっています。
コンパイラ
コンパイラは、プリプロセッサによって前処理が終わったプログラムを解釈して、アセンブリに変更します。アセンブリは、いわゆる機械語を人間の目でもわかりやすいように表現したような形式になっており、各CPUアーキテクチャがそれぞれアセンブリの形式を持っています。
アセンブラ
アセンブラとは、アセンブリ言語を、実際に機械語の表記である、オブジェクトファイルに変換するものです。
コンパイラドライバ
gccなどは、コンパイラドライバとして、コンパイラ・アセンブラ・リンカ・ローダーがすべてバンドルされたツールセットになっています。