コードをコンパイル後直ちに実行するためのメカニズム

コンパイラ型言語環境を作ってみたいという人には参考になるかもしれない話、ということで、iMopsでのコード実行のメカニズム、特に、メモリーの使用法について、説明してみようと思います。

アプリケーションの起動実行

初歩的なことですが、普通のアプリケーションプログラムの実行コードは、実行可能ファイルとして、アプリケーションバンドル(束)の中に紛れ込んでいます。これがダブルクリックで実行されるわけですが、この過程は、見た目は単純ですが、実はかなり複雑な手続きを経た上で実現されています。これについてあまり詳しく知る必要も無いのですが、ともかく、これは、OSのサービスとして提供されているのが一般的です。

実行可能ファイルには、“この中に実行可能コード(マシン語の符号)が入ってるよ”、という符牒が、ヘッダデータという形で入っています。ダブルクリックされたとき、OSはその符牒があるはずの場所を確認して、「あ、なんかのアプリで開くんじゃなくて、コレ自体を実行するのか」と分かるわけです。これを受けて、ローダーなるソフトウェアが、そのファイルをメモリーのどこか実行可能メモリー領域にコピーします。で、実行可能ファイルに書き込まれているデータから、必要な外部関数を含んだライブラリー(これも実行可能ファイルですが単独実行は出来ない)を知って、それもメモリー上のライブラリー領域にコピーした上で、必要な関数にリンクします。関数へのリンクというのは、その関数の内容コードのメモリー位置の頭の部分を割り出し、そのメモリーの番地の数値(メモリーアドレス)を、それ用に準備したマス(メモリーセル)の中に書き込むという操作にあたります。そして最後に、やはりこれもヘッダーデータに書き込まれてあるのですが、そのアプリケーションの最初に実行されなければならない関数、main()とか、それに似た類の名前の特別な関数になりますが、それがロードされた位置を割り出します。そして、そこから実行してよ、とコンピュータにお願いするわけです。こうして、アプリケーションは起動されます。

上に、関数のメモリーアドレスを割り出すというのがありますが、これは、普通は相対アドレス(メモリーオフセット)を用います。一つの実行可能ファイルのロード(メモリーへのコピー)では、論理上ひとつながりのメモリー領域に、べったりそのままの順番でファイル内容がコピーされます。すると、ファイルの始めの位置からどれだけ後の方にその関数が記載されているかという、ズレ分(オフセット)バイト数がわかれば、そのファイルがメモリー上にコピーされた位置の頭のアドレスにその数値を足せば、メモリーにコピーされた関数のコードのアドレスがわかります。

例えばmain()関数のアドレスを割り出すというときには、実行可能ファイルのヘッダデータには、ファイルの頭からmain()関数の内容が記載されているところまでのずれのバイト数が記載されています。仮に、1000バイトだったとします。すると、OSが提供する実行補助ソフトウェアは、そのずれの数値データを読み取って、コピーされたメモリー領域の始まりの部分のメモリーアドレスに足し算します。今、コードファイルがロードされたはじめの位置のアドレスが4096(メモリーアドレスの単位もバイトです)だったとします。すると、main()関数のコードは4096+1000=5096の位置にあります。補助ソフトウェアは、ちょうどそこにコードの実行を飛ばす(ジャンプさせる)わけです。コンピュータは、飛ばされた先の先頭から順にマシンコードを読み取り、アプリケーションソフトウェアの内容が、一つずつ実行される過程に入ります。

このような起動時の実行は、やや大掛かりなソフトウェアを動員して実現されますが、基本的なアイディアは、アプリケーション実行中の関数の呼び出しと同じです。実行内の過程は、もうマシン語として組み込まれた命令になっているというところが違うだけ、ともいえます。

Mopsの実行コード

Mops(PowerMopsもiMopsも同じ)の実行コードは、通常のアプリケションと同様、実行可能ファイルの中に書き込まれており、起動されると、上に書いたのと同じような動作で実行が始まります。

Mopsでは、立ち上がりのコードで、実行可能ファイルの内容を、OSによって自動的にロードされたのとは別のメモリー領域に丸ごとコピーします。後者のメモリー領域は、これからコードがコンパイルされて実行もされる辞書の部分に当たるわけですから、もちろん、読み出し、書き込み、実行のすべてが可能なように指定しておきます。コピーしたデータの後に数MBの空き領域を確保してあって、これが追加定義されるワードのための辞書(ディクショナリ)領域となります。そして、コピー領域の準備ができたところで、ちょうど今実行している箇所の直後に対応するコピーの位置にジャンプします。そうすることで、実行は、ロードされた実行可能ファイル内から飛び出して、コピーされた実行可能コードを実行し出すことになります。実行可能コード内のジャンプは、外部関数呼び出し以外は、すべて相対位置で指定するジャンプなので、コピーで全体の位置が平行してずれても、全く同じように実行できるわけです。

実行コードがコピーされたメモリー領域の残りの空き領域の始まりのメモリーアドレスを、立ち上げ時にVALUE変数に格納しておきます。コンパイルされるワードは、このアドレスから後に格納されていき、コードがコンパイルされる都度、アドレスの値は更新されていきます。これに応じて、空き領域の始まりは、辞書領域の終わりの方に向かって進行して行きます。

変数などデータを含む部分の辞書も、実行ジャンプを除けば、上と同じように、実行可能ファイル上のデータがヒープにコピーされて、コピー部分が使用されます。

解釈実行

さて、ENTERキーによって引き起こされるMopsの解釈実行は、基本的に、まずソースをマシン語にコンパイルし、それを直ちに実行することによって行なわれます。ソーステキストが直接に動作を引き起こす形にはなっていません。
基本は1ワードごとにコンパイル・実行しています。

前の節の説明からは抜けていますが、起動時、イベント待ちの状態になる前に、辞書領域とは別にさらに1ページ(4kb)のヒープ領域を確保して、その領域を実行可能(当然、読み書きも可能)指定をしておきます。これをバッファーとして、この領域中に、解釈実行用のマシン語がコンパイルされます。解釈実行ワードは、パラメター等の保存・付け替えを適宜した上で、まずバッファー上にマシン語をコンパイルし、それが終わった後、パラメター等を元に戻して実行の準備を整え、コードの先頭に実行を飛ばします。

古典的なforthでは、辞書内の各ワードに対応するコードにジャンプするのは、インタープリターワードの仕事でしたが、Mopsのインタープリターワードは、各ワードのコードにはジャンプせず、呼び出しをコンパイルしたマシン語が置かれているバッファーに一回だけジャンプします。各ワードへのジャンプは、バッファー上にコンパイルされたマシンコードの中のcallの機械命令によって行なわれます。

バッファーは、少しずつズラしながら利用して行きます。基本的には、1ワードごとにコンパイル・実行を行うので、一回のコードの長さはごく小さなものになります。コンパイル開始点がバッファー領域の終端に近づいてきたら、適宜、巡回的に開始点を先頭に戻します。

マシンコードをメモリーに記録した直後に実行するやり方は、あまり効率のいいやり方ではありませんし、場合によっては、ストールが生じることもあるでしょうが、インタープリトの過程は、さほど高速な処理が必要なものでもありませんし、コンパイル後のパラメター復帰などメモリーへのアクセスによって、コンパイルと実行の間に若干の間が空くことは避けられないので、不都合が生じることはないようです。実際、コードのコンパイルもこの同じ解釈実行過程を経由して行なわれますが、Mopsのコンパイルは、相当な最適化操作を含むにもかかわらず、非常に高速です。iMopsのカーネルビルドは、500kb程度の最適化されたマシンコードを生成しますが、ビルドのほとんどの時間はメッセージの文字列表示のために費やされ、実質は1秒もない程度です。



(以下鋭意作成中)


最終更新:2020年03月02日 15:04