web-dev-qa-db-ja.com

独自の言語でコンパイラを書く

直感的には、言語Fooのコンパイラ自体をFooで書くことはできないようです。具体的には、言語Fooの-​​firstコンパイラはFooで記述できませんが、後続のコンパイラはFooで記述できます。

しかし、これは実際に本当ですか?私は、最初のコンパイラが「自身」で書かれた言語について読んだことについて、非常に曖昧な思い出があります。これは可能ですか?

190
Dónal

これは「ブートストラップ」と呼ばれます。最初に、他の言語(通常JavaまたはC))で使用する言語のコンパイラ(またはインタープリター)をビルドする必要があります。それが完了したら、新しいバージョンのコンパイラを言語Fooで記述できます。 。最初のbootstrap=コンパイラーを使用してコンパイラーをコンパイルし、このコンパイル済みコンパイラーを使用して他のすべて(それ自体の将来のバージョンを含む)をコンパイルします。

ほとんどの言語は実際にこの方法で作成されます。これは、言語設計者が作成している言語を使用することを好むこと、および非自明なコンパイラが言語の「完成度」の有用なベンチマークとして役立つことが多いためです。

これの例はScalaです。最初のコンパイラは、Martin Oderskyによる実験言語であるPizzaで作成されました。バージョン2.0の時点で、コンパイラはScalaで完全に書き直されました。この時点から、新しいScalaコンパイラーを使用して将来の反復用にコンパイルできるため、古いPizzaコンパイラーは完全に破棄されます。

221
Daniel Spiewak

ソフトウェアエンジニアリングラジオポッドキャスト を聞いたことを思い出します。ここで、ディックガブリエルは、LISPに必要最低限​​のバージョンを記述して、元のLISPインタープリターをブートストラップすることについて話しました紙の上コード。それ以降、残りのLISP機能はLISPで記述され、解釈されました。

71
Alan

以前の回答に好奇心を加えます。

ソースからGCCコンパイラの構築を開始する段階での Linux From Scratch マニュアルからの引用です。 (Linux From Scratchは、ディストリビューションのインストールとは根本的に異なるLinuxをインストールする方法です。ターゲットの実際のevery単一のバイナリをコンパイルする必要がありますシステム。)

make bootstrap

「ブートストラップ」ターゲットは、GCCをコンパイルするだけでなく、数回コンパイルします。最初のラウンドでコンパイルされたプログラムを使用して、2回目のコンパイルを行い、3回目のコンパイルを繰り返します。次に、これらの2番目と3番目のコンパイルを比較して、完全に再現できることを確認します。これは、正しくコンパイルされたことも意味します。

「ブートストラップ」ターゲットの使用は、ターゲットシステムのツールチェーンを構築するために使用するコンパイラが、ターゲットコンパイラとまったく同じバージョンを持たない可能性があるという事実に基づいています。そのように進めると、ターゲットシステムで、自分自身をコンパイルできるコンパイラを確実に取得できます。

46

C用の最初のコンパイラを作成するとき、他の言語で作成します。これで、たとえばアセンブラーにC用のコンパイラーができました。最終的に、文字列、特にエスケープシーケンスを解析する必要があります。 \nを10進コード10(および\rを13など)に変換するコードを作成します。

そのコンパイラの準備ができたら、Cでコンパイラの再実装を開始します。このプロセスは「 bootstrapping 」と呼ばれます。

文字列解析コードは次のようになります。

...
if (c == 92) { // backslash
    c = getc();
    if (c == 110) { // n
        return 10;
    } else if (c == 92) { // another backslash
        return 92;
    } else {
        ...
    }
}
...

これがコンパイルされると、 '\ n'を理解するバイナリが得られます。これは、ソースコードを変更できることを意味します。

...
if (c == '\\') {
    c = getc();
    if (c == 'n') {
        return '\n';
    } else if (c == '\\') {
        return '\\';
    } else {
        ...
    }
}
...

では、「\ n」が13のコードであるという情報はどこにありますか?それはバイナリです! DNAのようなものです。このバイナリでCソースコードをコンパイルすると、この情報が継承されます。コンパイラがそれ自体をコンパイルする場合、この知識はその子孫に渡されます。この時点から、ソースのみからコンパイラが何をするかを確認する方法はありません。

いくつかのプログラムのソースでウイルスを隠したい場合は、次のようにすることができます:コンパイラのソースを取得し、関数をコンパイルする関数を見つけて、これに置き換えます:

void compileFunction(char * name, char * filename, char * code) {
    if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
        code = A;
    } else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
        code = B;
    }

    ... code to compile the function body from the string in "code" ...
}

興味深い部分はAとBです。Aはウイルスを含むcompileFunctionのソースコードであり、おそらく何らかの方法で暗号化されているため、結果のバイナリを検索しても明らかではありません。これにより、コンパイラー自体でコンパイルすることにより、ウイルスインジェクションコードが確実に保持されます。

Bは、ウイルスで置き換えたい機能でも同じです。たとえば、ソースファイル "login.c"の関数 "login"である可能性があります。これは、おそらくLinuxカーネルからのものです。通常のパスワードに加えて、rootアカウントのパスワード「joshua」を受け入れるバージョンに置き換えることができます。

それをコンパイルしてバイナリとして拡散した場合、ソースを見てウイルスを見つける方法はありません。

アイデアの元のソース: http://cm.bell-labs.com/who/ken/trust.html

41
Aaron Digulla

開始ソースコードをコンパイルするものがないため、コンパイラ自体を作成することはできません。これを解決する方法は2つあります。

最も好ましくないのは次のとおりです。言語の最小限のセット用にアセンブラー(yuck)で最小限のコンパイラーを作成し、そのコンパイラーを使用して言語の追加機能を実装します。すべての言語機能を備えたコンパイラーが完成するまでの道のりを構築します。通常、他に選択肢がない場合にのみ行われる痛みを伴うプロセス。

推奨されるアプローチは、クロスコンパイラを使用することです。別のマシン上の既存のコンパイラのバックエンドを変更して、ターゲットマシンで実行される出力を作成します。次に、ニースの完全なコンパイラを起動して、ターゲットマシンで作業します。これに最も人気があるのはC言語です。これは、交換可能なプラグイン可能なバックエンドを備えた既存のコンパイラがたくさんあるためです。

少し知られている事実は、GNU C++コンパイラにはCサブセットのみを使用する実装があります。通常、新しいターゲットマシン用のCコンパイラを見つけやすく、それから、完全なGNU C++コンパイラをビルドします。これで、ターゲットマシンにC++コンパイラをインストールできるようになりました。

18
Phil Wright

一般に、最初に動作する(プリミティブな場合)コンパイラーの作業を停止する必要があります-その後、自己ホスト化することについて考え始めることができます。これは実際、いくつかの言語では重要なマイルストーンと見なされています。

私が「モノ」から覚えていることから、それを機能させるにはリフレクションにいくつかのことを追加する必要がありそうです:モノチームはReflection.Emitでは不可能なことを指摘し続けます。もちろん、MSチームはそれらが間違っていることを証明するかもしれません。

これにはいくつかのrealの利点があります:まず最初に、かなり良い単体テストです!心配する言語は1つだけです(つまり、C#の専門家はC++をあまり知らない可能性がありますが、C#コンパイラを修正できるようになりました)。しかし、私はここで仕事にプロのプライドの量がないのではないかと思います:彼らは単にwantそれがセルフホスティングであることです。

コンパイラーではありませんが、私は最近、自己ホスト型のシステムに取り組んでいます。コードジェネレーターは、コードジェネレーターの生成に使用されます。そのため、スキーマが変更された場合は、単に新しいバージョンで実行します。バグがある場合は、以前のバージョンに戻って再試行します。非常に便利で、保守が非常に簡単です。


アップデート1

PDCのAndersの このビデオ をご覧になりましたが、(約1時間後)彼はもっと多くの正当な理由を述べています-サービスとしてのコンパイラーについて。記録のためだけに。

14
Marc Gravell

以下にダンプを示します(実際に検索するのは難しいトピックです)。

これは、 PyPy および Rubinius の考え方でもあります。

(これは Forth にも当てはまると思いますが、Forthについては何も知りません。)

4
Gene T

GNAT、GNU Adaコンパイラーは、Adaコンパイラーを完全にビルドする必要があります。これは、GNATバイナリーがすぐに利用できないプラットフォームに移植する場合、苦痛になります。

1
David Holm

MonoプロジェクトのC#コンパイラは、長い間「セルフホスト」されてきました。つまり、C#自体で記述されているということです。

私が知っていることは、コンパイラは純粋なCコードとして開始されましたが、ECMAの「基本」機能が実装されると、C#でコンパイラを書き直し始めたことです。

私は同じ言語でコンパイラを書く利点を認識していませんが、少なくとも言語自体が提供できる機能を実行する必要があると確信しています(たとえば、Cはオブジェクト指向プログラミングをサポートしません) 。

より多くの情報を見つけることができます こちら

1
Gustavo Rubio

実際、ほとんどのコンパイラは、上記の理由により、コンパイルする言語で書かれています。

最初のbootstrap=コンパイラは通常、C、C++、またはアセンブリで記述されています。

1
Can Berk Güder

たぶん、BNFを記述する [〜#〜] bnf [〜#〜] を書くことができます。

0
Eugene Yokota