このページの目次

rvtl のソースコード解説

rvtlコンパイラのソースコードの解説を書いてみました。rvtlc は簡単なコンパイラでソースコードは2000行ほどです。普通のプログラミング言語のコンパイラでは、字句解析、構文解析、意味解析、エラー処理、コード生成、最適化、リンクなどのステップを経て実行ファイルに変換します。rvtl は文法が簡単なため、rvtl コンパイラではソースを1文字ずつ読みながら、直接実行形式に変換するため非常に高速にコンパイルできます。最近のPCならば1分間に100万行のコンパイルが可能と思います。ほとんど最適化していないため実行速度は速くはありませんが、数キロバイトから数十キロバイト程度のコンパクトな実行ファイルとなります。


rvtlc.vtl は rvtl上で動作する rvtl で書かれたコンパイラです。Linuxの実行ファイルを直接出力します。したがってリンカ(ld)もアセンブラも不要です。出力された実行ファイル はそのファイル単独で動作し、libc等の共有ライブラリは不要です。


最新版のソースのダウンロードはこちらのページから。


rvtl自身でrvtlのコンパイラを記述した意味は以下のように考えることができます。

1) オリジナル言語を考える
    今回は rvtl という TINY BASIC言語系の言語です。

2) その言語のインタープリタを作成
    アセンブラで rvtl のインタープリタを作成しました。

3) その言語のコンパイラのソースをインタープリタでコンパイル
    rvtl言語でコンパイラのソースを書いてインタープリタでコンパイルすると
    rvtl言語コンパイラの実行形式のファイルができます。

4) コンパイルされたコンパイラでコンパイラのソースをコンパイル
    実行形式のrvtlコンパイラでコンパイラのソースがコンパイルできることを
    確認します。この時点からrvtlコンパイラのソースのコンパイルに
    rvtlインタープリタは不要になります。

5) コンパイラのソースを改良してコンパイル
    rvtlコンパイラだけでrvtlコンパイラの改良や言語自身の改良が可能です。

プログラム言語を作成するには、インタプリタやコンパイルを記述するためのプログラム言語が必要となります。「タマゴが先か、ニワトリが先か」という問題と同じです。「ニワトリではない鳥がニワトリのタマゴを産んだ」が答えとなります。

rvtlcの出力ELFバイナリのメモリマップ

rvtlcが出力するプログラム(実行形式のELFファイル)の実行時メモリマップです。gcc とは異なり、MS-DOS のcom形式のように固定アドレスで動作します。

        :                                       :
        +---------------------------------------+ 0x08048000
  .text | ELF Header領域                        |
        +---------------------------------------+ 0x08048080
        | ランタイムライブラリ領域              |
        +---------------------------------------+
        | コード領域 最大350KB                  |
        |                                       |
        +---------------------------------------+
        :                                       :
        +=======================================+ 0x080A0000
  .bss  | ランタイムライブラリ作業領域   16KB   |
        +---------------------------------------+ 0x080A4000
        | 変数領域          128ワード     1KB   |
        +---------------------------------------+
        | 変数スタック     1024ワード     4KB   |
        +---------------------------------------+ &
        | ヒープ領域  256KB 実行時に拡張可能    |
        :                                       :
        |                                       |
        +---------------------------------------+ *
        :                                       :
        | スタック領域                          |
        +---------------------------------------+

変数の割付

rvtlは変数として A-Z、a-z の52種類のみ使用できます。rvtlc.vtl では A-Z、z をグローバル変数として使い、a-y を局所変数として使っています。

グローバル変数

rvtlコンパイラの全体を通して使うグローバル変数の使用方法です。

A 未使用
B BSS .bss 先頭アドレス
C 入力ソース文字配列、行長はSに保持
D デバッグレベル
E Lib内の.text領域書き込み用ワーク
F ライブラリ読み込みエリア
G ライブラリサイズ
H ELFフォーマットワーク(配列)
I セクションヘッダテーブルの設定で使用(局所変数で可)
J ランタイムライブラリのジャンプテーブルオフセット
K ラベル:アドレステーブル最終登録位置+1=登録数 を保持
L ラベル:アドレステーブル
M コード領域先頭
N Nは行先頭ポインタを最初の行に設定
O Obj[] コード格納配列
P Objのインデックス(byte)
Q PASS カウンタ
R PASS毎のコードサイズを保持
S Cからの行長
T Control Table (配列)
U Control Table Pointer
V 行番号:アドレステーブル
W 行番号:アドレステーブル最終登録位置+1=登録数 を保持
X 出力ファイル名
Y ソース格納用領域
Z コンパイラ先頭行番号を保存
z 確保済みのメモリ上限+1

グローバル変数(配列H)

グローバル変数の数が足らないため配列Hをグローバル変数として使います。読みやすさのため、インデックスは数値ではなく文字定数を使っています。ちょっとメモリの無駄になりますが、識別しやすくなります。

H['A']=$08048080 e_entry
H['B']=$08048000 address .text
H['C']=P コード生成先頭オフセット
H['D']=q-Obj-P .data文字列
H['E']=q-Obj-P .bss文字列
H['F'] ソースファイルディスクリプタ
H['G'] ソース読み込み用ワーク
H['H']=q-Obj-P .shstrtab文字列
H['I'] 開始時間記録(秒)
H['J'] 開始時間記録(マイクロ秒)
H['K']=$080A0000 .data 領域先頭アドレス
H['L']=0 .data 領域のサイズ(byte)
H['M']=0 .data オフセット
H['N']=H['K']+H['L'] .bss 領域先頭アドレス
H['O']=256*1024 .bss 領域のサイズ(byte)
H['P']=H['A']-H['B'] sect_offset=e_entry-p_paddr
H['Q']=P .data 終わり
H['R']
H['S']=P .shstrtab
H['T']=q-Obj-P .text文字列
H['U']=$4000 .bss領域中の変数領域オフセット
H['V']=H['N']+H['U'] .bss領域中の変数領域先頭アドレス
H['W']=H['V']+$400 .bss領域中の変数スタック先頭アドレス
H['X']=H['W']+$1000 ヒープ領域先頭アドレス
H['Y']=P-H['S'] .shstrtabセクションサイズ
H['Z']=P-H['P'] .textセクションサイズ

配列用のメモリ確保とグローバル変数の初期設定

rvtlcの初期化部分です。変数 z を空きメモリ領域の先頭に設定して、配列の領域を確保する場合は、「X=z z=z+256」のように z を更新します。これで「X(256)」に相当する動作をします。

10000 :------------------------------------------------------------
10010 : rvtlc.vtl - rvtl Compiler
10020 : 2006/03/20-    Jun Mizutani
10030 :   2006/05/02,05/16
10040 : コンパイルするrvtlソースをハイフンの後に指定
10050 :  rvtl rvtlc.vtl - to_be_compiled.vtl
10060 :  #=1
10070 : PASS1でコード生成とラベルテーブル作成
10080 : PASS2でラベルテーブルを参照してコード生成、バイナリ出力
10090 :------------------------------------------------------------
10100   Z=9999999 : #-100               : コンパイラ先頭行番号を保存
10110   D=0                             : デバッグレベル
10120   z=&                             : ヒープ先頭
10130   X=z z=z+256 X*="bin.elf"        : 出力ファイル名(後で変更)
10140   M==                             : コード領域先頭
10150   *=*+(512*1024)
10160   J=35                            : ジャンプテーブル先頭インデックス
10170   :
10180   Obj=z z=z+(256*1024)            : 256KB までのバイナリファイル
10190   H=z z=z+512                     : for ELF Header
10200   T=z z=z+300                     : control table (for,do)
10210   U=0                             : control table pointer
10220   V=z z=z+64000                   : 8000行分の行番号:アドレステーブル
10230   W=0                             : 行番号テーブル最終位置+1=登録数
10240   L=z z=z+16000                   : 1000のラベル:行番号テーブル
10250   K=0                             : ラベル:行番号テーブル最終位置+1=登録数
10260   Q=1                             : PASSカウンタ
10270   F=z z=z+8096                    : ライブラリ読み込み領域
10280   R=z z=z+8                       : パス間のコードサイズ確認
10290   !=^TimerStart
10300   !=^InitSource                   : ソースコード読み込み
10310   :-------------------
10320   H['B']=$08048000                : コード領域先頭アドレス
10330   H['A']=$08048080                : e_entry
10340   H['K']=$080A0000                : .data 領域先頭アドレス
10350   H['L']=0                        : .data 領域のサイズ(byte)
10360   H['M']=0                        : .data オフセット
10370   H['N']=H['K']+H['L']            : .bss  領域先頭アドレス
10380   H['O']=256*1024                 : ヒープ領域のサイズ(byte)
10390   H['U']=$4000                    : .bss領域中の変数領域オフセット
10400   H['V']=H['N']+H['U']            : .bss領域中の変数領域先頭アドレス
10410   H['W']=H['V']+$400              : .bss領域中の変数スタック先頭アドレス
10420   H['X']=H['W']+$1000             : ヒープ領域先頭アドレス
10430   !=^LibRead                      :
10440   !=^SetUpLib
10450   E=Obj+P
10460   H['C']=P                        : コード生成先頭オフセット
10470   H['P']=H['A']-H['B']            : sect_offset=e_entry-p_paddr
10480   :

行番号10110の変数 D の値を大きくすると構文解析の経過やコンパイルの結果の詳細を表示することができます。値が大きくなると表示内容は追加されていきます。

D 表示内容
0 表示なし
1 行番号テーブルとラベルテーブルを表示
2 構文解析の経過を表示
3 ソース中の数値を表示
4 ランタイムライブラリの呼び出し番号を表示
5 アドレスと出力する機械語を表示

時間計測

コンパイル時間を計測する部分。コンパイル前に時刻を取得し、コンパイル後の時刻との差を求めます。時刻はUNIX時間とマイクロ秒で返るため、それぞれ差を求めています。

30370 :----------------------------------
30380 : 時間計測開始
30390 :----------------------------------
30400 ^TimerStart
30410     H['I']=_  H['J']=%
30420 ]
30430 :
30440 :----------------------------------
30450 : 時間計測終了、経過時間表示
30460 :----------------------------------
30470 ^TimerStop
30480     t=H['I'] u=H['J'] x=_ y=%
30490     d=x-t f=y-u
30500     ;=(f<0) d=d-1 f=f+1000000
30510     "  time:" ?=d "." ?[6]=f "sec" /
30520 ]
30530 :

ソースコードの読み込み

コマンドライン引数で渡されたソースファイルをオープンして、一文字づつ読み込み、以下の形式で行として格納します。行番号は読込み時点で数値に変換されます。rvtl インタプリタがスクリプトをメモリ中に格納する形式と同じです。

            4byte          4byte                       1byte     0-3byte
     +-------------------+--------+--------------------+-----+-----------+
 +---| 次行へのオフセット| 行番号 |     行の内容       | 0   | アライン用|
 |   +-------------------+--------+--------------------+-----+-----------+
 +-->| 次行へのオフセット| 行番号 |     行の内容       | 0   | アライン用|
     +-------------------+--------+--------------------+-----+-----------+
     :                   :        :                    :     :           :
     +-------------------+--------+--------------------+-----+-----------+
     |     -1            |        |                    |     |           |
     +-------------------+--------+--------------------+-----+-----------+

30540 :-------------------------------------------------
30550 : ソース読み込み
30560 :-------------------------------------------------
30570 ^InitSource
30580 :  z=&
30590   Y=z       z=z+(128*1024)      : ソース保存領域
30600   H['G']=z  z=z+16              : work
30610   !=^LoadSource
30620   !=^MkFileName
30630 :  !=^ListSource
30640   M=Y                           :
30650 ]
30660 :
30670 :-------------------------------------------------
30680 : 出力ファイル名をXに設定
30690 :-------------------------------------------------
30700 ^MkFileName
30710    +bzinf
30720    b=z z=z+256
30730    [=0 b*=\0
30740    n=%
30750    i=n
30760    @
30770      i=i-1
30780    @=((b(i)='.')|(i=0))
30790    ;=i=0 f=b+n f*=".elf" X*=b ]
30800    f=b+i f*=".elf" X*=b
30810    -fnizb
30820 ]
30830 :
30840 :-------------------------------------------------
30850 : ソースファイルを読み込み
30860 :-------------------------------------------------
30870 ^LoadSource
30880    +ynml
30890    !=^SourceOpen         : ソースファイルをオープン
30900    y=Y
30910    l=0
30920    @
30930      : 1行読み込み
30940      !=^SourceRead
30950      ;=c=' ' !=^SkipSpace
30960      ;=(c<'0')|(c>'9') !=^SkipLine #=^ls0
30970      !=^GetLineNo        : 行番号
30980      y[1]=n
30990      m=8
31000      @
31010        y(m)=c            : ソースをLFまで保存
31020        m=m+1
31030        !=^SourceRead
31040        ;=c=$0D !=^SourceRead
31050      @=((c=$0A)|(c=0))
31060      y(m)=0 m=m+1
31070      m=(m+3)/4*4         : 4でアライン
31080      y[0]=m              : 次行までのオフセットを設定
31090      y=y+m
31100      l=l+1
31110     ^ls0
31120    @=(c=0)
31130    y[0]=-1
31140    !=^SourceClose        : ソースファイルをクローズ
31150    / "read " ?=l " lines" /
31160    -lmny
31170 ]
31180 :
31190 :-------------------------------------------------
31200 : 1行読み飛ばし
31210 ^SkipLine
31220    @
31230      !=^SourceRead
31240    @=((c=$0A)|(c=0))
31250 ]
31260 :
31270 :-------------------------------------------------
31280 : ソース表示
31290 :-------------------------------------------------
31300 ^ListSource
31310    y=Y
31320    n=0
31330    @
31340      ?(6)=y[1]
31350      $*=y+8 /
31360      y=y+y[0]
31370    @=(y[0]=-1)
31380 ]
31390 :
31400 :-------------------------------------------------
31410 : 空白除去
31420 :-------------------------------------------------
31430 ^SkipSpace
31440     @
31450       !=^SourceRead
31460       ;=(c>='0')&(c<='9') n=n*10+(c-'0')
31470     @=((c<>' '))
31480 ]
31490 :
31500 :-------------------------------------------------
31510 : 行番号取得 cに1文字目
31520 :-------------------------------------------------
31530 ^GetLineNo
31540     n=c-'0'
31550     @
31560       !=^SourceRead
31570       ;=(c>='0')&(c<='9') n=n*10+(c-'0')
31580     @=((c<'0')|(c>'9'))
31590 ]
31600 :

rvtlの基本機能ではファイルをすべてメモリに読み込むことはできますが、「ファイルをオープンして、ファイルから1文字ずつ読み込み、ファイルをクローズする」機能はありません。ソースファイルから1文字ずつ読み込むため、組み込みコマンド|zz を使ってカーネルのシステムコールで、ファイルオープン、ファイルリード、ファイルクローズを実現しています。

31610 :-------------------------------------------------
31620 : ソースファイルをオープン
31630 :-------------------------------------------------
31640 ^SourceOpen
31650    +r
31660    b=\0
31670    !=^fropen
31680    H['F']=r
31690    -r
31700 ]
31710 :
31720 :-------------------------------------------------
31730 : ソースファイルをクローズ
31740 :-------------------------------------------------
31750 ^SourceClose
31760    b=H['F']
31770    !=^fclose
31780 ]
31790 :
31800 :-------------------------------------------------
31810 : ソースから1文字読み込み
31820 :-------------------------------------------------
31830 ^SourceRead
31840   a=3               : read
31850   b=H['F']          : fd
31860   c=H['G']          : buffer
31870   d=1               : size
31880   |zz
31890   c=c(0)
31900   ;=(|<=0) c=0
31910 ]
31920 :
31930 :-------------------------------------------------
31940 : ファイルをオープン
31950 : enter   b : 第1引数 filename
31960 : return  r : fd, if error then eax will be negative.
31970 :-------------------------------------------------
31980 ^fropen
31990    c=0              : 第2引数 flag=O_RDONLY
32000    #=^fopen
32010 ^fwopen
32020    c=577            : O_CREAT|O_WRONLY|O_TRUNC
32030 ^fopen
32040    a=5              : システムコール番号 SYS_open
32050    d=420            : 第3引数 mode=0644
32060    |zz
32070    r=|
32080 ]
32090 :-------------------------------------------------
32100 : ファイルをクローズ
32110 : enter   b : 第1引数 ファイルディスクリプタ
32120 :-------------------------------------------------
32130 ^fclose
32140    a=6              : システムコール番号 SYS_close
32150    |zz
32160    r=|
32170 ]
32180 :

[目次] [次]