私は主に、gccなどの一般的で広く使用されているコンパイラに興味があります。しかし、コンパイラーによって処理が異なる場合は、それも知りたいと思います。
Gccを例にとると、Cで記述された短いプログラムを直接machineコードにコンパイルしますか、それとも最初に人間が読めるアセンブリに変換してから(組み込み?)を使用しますか?アセンブリプログラムをバイナリに変換するアセンブラ、machine code-CPUへの一連の命令?
アセンブリコードを使用してバイナリ実行可能ファイルを作成すると、非常にコストのかかる操作になりますか?それとも、比較的簡単で迅速なことですか?
(x86ファミリのプロセッサのみを扱っており、すべてのプログラムがLinux用に作成されていると仮定しましょう。)
私はこの問題についての助けと考えに非常に感謝します。ありがとうございました!
gccは実際にアセンブラを生成し、asアセンブラを使用してアセンブラします。すべてのコンパイラがこれを行うわけではありません。MSコンパイラはオブジェクトコードを直接生成しますが、アセンブラ出力を生成させることもできます。アセンブラをオブジェクトコードに変換することは、少なくともコンパイルと比較すると、非常に単純なプロセスです。
一部のコンパイラは、出力として他の高級言語コードを生成します。たとえば、cfront、最初のC++コンパイラは出力としてCを生成し、Cコンパイラによってコンパイルされました。
直接コンパイルもアセンブリも実際には実行可能ファイルを生成しないことに注意してください。これはlinkerによって実行されます。これは、コンパイル/アセンブリによって生成されたさまざまなオブジェクトコードファイルを取得し、それらに含まれるすべての名前を解決して、最終的な実行可能バイナリを生成します。
Gccを含むほとんどすべてのコンパイラは、コンパイラの生成とデバッグの両方が簡単であるため、アセンブリコードを生成します。主な例外は通常、ジャストインタイムコンパイラまたはインタラクティブコンパイラであり、その作成者は、パフォーマンスのオーバーヘッドや、プロセス全体をフォークしてアセンブラを実行する煩わしさを望んでいません。いくつかの興味深い例が含まれます
ニュージャージーの標準ML 、インタラクティブに実行され、すべての式をその場でコンパイルします。
tinyccコンパイラ は、100ミリ秒未満でCスクリプトをコンパイル、ロード、および実行するのに十分な速度で設計されているため、アセンブラとリンカを呼び出すオーバーヘッドを必要としません。
これらのケースに共通しているのは、「瞬時の」対応への欲求です。アセンブラとリンカは十分に高速ですが、インタラクティブな応答には十分ではありません。それでも。
Smalltalk、Java、 Lua など、アセンブリコードではなくバイトコードにコンパイルされる言語の大規模なファミリもありますが、その実装では、後でそのバイトコードをマシンコードに直接変換する可能性があります。アセンブラ。
(脚注:1990年代初頭、メアリーフェルナンデスと私は ニュージャージーマシンコードツールキット を作成しました。 コード はオンラインであり、コンパイラの作成者が使用できるCライブラリを生成します。メアリーは、これを使用して、a.out
を生成するときに、最適化するリンカーの速度を約2倍にしました。ディスクに書き込まない場合、速度はさらに向上します...)
一般に、コンパイラーはソースコードを抽象構文木(AST)に解析し、次に中間言語に解析します。その場合にのみ、通常はいくつかの最適化の後、ターゲット言語を出力します。
Gccについては、さまざまなターゲットにコンパイルできます。 x86の場合、最初にアセンブリにコンパイルされるかどうかはわかりませんが、コンパイラについての洞察を提供しました。あなたもそれを求めました。
第2章 of リバースエンジニアリングソフトウェアの概要 (MikePerryとNaskoOskovによる)によると、gccとcl.exe(MSVC++のバックエンドコンパイラ)の両方に-Sスイッチを使用して、各コンパイラが生成するアセンブリを出力できます。
Gccを冗長モードで実行することもできます(gcc -v
)実行するコマンドのリストを取得して、舞台裏で何が行われているかを確認します。
GCCはアセンブラにコンパイルされます。他のいくつかのコンパイラはそうではありません。たとえば、LLVM-GCCはLLVM-AssemblyまたはLLVM-bytecodeにコンパイルされ、次にマシンコードにコンパイルされます。ほとんどすべてのコンパイラにはある種の内部表現があり、LLVM-GCCはLLVMを使用し、IIRC、GCCはGIMPLEと呼ばれるものを使用します。
アセンブラがバイナリコードとマシン依存のシンボリックコードの間の抽象化の最初の層であるという事実を明確にする答えはありません。コンパイラーは、MACHINE DEPENDENT SYMBOLICCODEとMACHINEINDEPENDENT SYMBOLICCODEの間の抽象化の第2層です。
コンパイラがコードをバイナリコードに直接変換する場合、定義上、コンパイラではなくアセンブラと呼ばれます。
コンパイラは、アセンブリ言語である場合とそうでない場合がある中間コードを使用すると言うのがより適切です。 Javaはバイトコードを中間コードとして使用し、バイトコードはJava仮想マシン(JVM)のアセンブラです。
編集:なぜアセンブラーが常にマシンに依存するコードを生成するのか、そしてなぜコンパイラーがマシンに依存しないコードを生成できるのか疑問に思うかもしれません。答えはとても簡単です。アセンブラはマシンコードの直接マッピングであるため、アセンブラが生成するアセンブリ言語は常にマシンに依存します。それどころか、異なるマシン用に複数のバージョンのコンパイラーを作成することができます。したがって、マシンから独立してコードを実行するには、同じコードをそのマシン用に作成されたコンパイラバージョンでコンパイルする必要があります。
Visual C++にはアセンブリコードを出力するための switch があるので、マシンコードを出力する前にアセンブリコードを生成すると思います。
コンパイルには多くのフェーズがあります。要約すると、ソースコードを読み取り、トークンに分割し、最後に解析ツリーに分割するフロントエンドがあります。
バックエンドは、最初に3つのアドレスコードのようなシーケンシャルコードを生成する責任があります。
コード:
x = y + z + w
に:
reg1 = y + z
x = reg1 + w
次にそれを最適化し、アセンブリに翻訳し、最後に機械語に翻訳します。すべてのステップは慎重に階層化されているため、必要に応じていずれかのステップを置き換えることができます
あなたはおそらくこのポッドキャストを聞くことに興味があるでしょう: GCCの内部
ほとんどの場合 マルチパスコンパイラ アセンブリ言語はコード生成ステップ中に生成されます。これにより、レクサー、構文、およびセマンティックフェーズを一度記述してから、単一のアセンブラーバックエンドを使用して実行可能コードを生成できます。これは、さまざまなCPU用に生成されるCコンパイラなどのクロスコンパイラでよく使用されます。
ほぼすべてのコンパイラには、暗黙的または明示的なステップに関係なく、何らかの形でこれがあります。
すべてのコンパイラがソースコードを中間レベルのコードに変換するわけではありませんが、いくつかのコンパイラではソースコードをマシンレベルのコードに変換するブリッジがあります。
JavaコンパイラはJavaバイトコード(バイナリ形式)にコンパイルしてから、仮想マシン(jvm)を使用してこれを実行します。
これは遅いように見えるかもしれませんが、JVMは後のCPU命令と新しい最適化を利用できるため、高速になる可能性があります。 C++コンパイラはこれを行いません-コンパイル時に命令セットをターゲットにする必要があります。