6301アセンブラの開発

f:id:PocketGriffon:20201025122515j:plain

アセンブラの開発記録っぽいものを書いてみようと思ったけれど…さすがに内容が専門的っぽくなるし、ブログの軽さに合わないかもなぁ…と感じてしまったので、箇条書き的に書いてます。
なんとなくな様子を感じ取ってもらえれば…(^^;;

 

開発したアセンブラ概要

開発したのは、おそらく標準的であろう2パスアセンブラ

 

コード表記については参考になるものが多くなかったので、「HC-20 100%活用法」や「6301マシン語入門」の表記を見て「こんな感じかな?」という感覚で決めている。他のCPUとは違い、カッコがあったり無かったりなどでアドレッシングの違いが出る表記ではない。

 

アセンブル前にソースをcpp(プリプロセッサ)に通す事を前提としている。
そのためcppの機能に頼り切ったアセンブラとなっている。
gcc -E file.asm で出力されたファイルのみアセンブル可能。
cppを通したファイルを引数で渡すもよし、「gcc -E test.asm | ./asm」という感じでパイプで渡すのもOK。

 

cppを通すという事で、#defineマクロ、#include、#ifdef等の条件コンパイルなどがそのまま利用出来る。C系言語に慣れた人であったら違和感のない使い方が出来るかと思う。
ただし別のファイルをincludeした際はエラーの行数表示がおかしくなる。

 

16進数は数値の先頭に$、または最後にh ($FFFF or 0FFFFh)。
2進数は最後にb。0101_1010b のように途中に'_'を入れる事が可能。

 

ラベルは必ず行の先頭に書き、長さは無制限だが便宜上256文字以下とした。最後は':'でしめる事。
ラベルは特別な手続きなしで後方参照が可能。
先頭に'_'を付けるとローカルラベルとなり、その関数(スコープ)内のみで参照が可能。

 

セグメントなどの概念はなく、書いた通りの順番でメモリに出力される。ソースの書き方次第ではコードとデータ、ワークが混在する。

 

分割アセンブルには対応していない。アセンブルするといきなりアドレスが確定したバイナリ(またはテキスト)が出力される。
メモリ範囲は$0000〜$FFFFの64KB固定。

 

開発的な話

開発言語はC言語
コメントや空白行を外した場合の総コード量は1000行以下とコンパクト。

 

字句解析/構文解析にlex/yaccを用い……ようかと思ったが、ツールを使うほど複雑ではないため自前で書いた。BNFが頭に入っていれば難しい話ではない。

 

{式}の解決については、内部で逆ポーランド式に変換して結果を得ている。
これは30年ほど前に書いたモジュールをいまだに使っているw
なんでも良いけどbison/flexと書かないと古い人らしい、今後は気をつけたいw

 

1パス目

1パス目はラベルの登録、仮の構文チェック、命令長の決定等を行っている。
命令長を調べるという事は、結構なところまで解析を行わなければならない。頑張ったらアセンブラ自体を1パスで作れるくらい。でも後方参照ラベルの解決など面倒な部分があるので、無理なく2パスにした…というだけの話。

 

ラベルの管理は、ラベルが出てくるたびに片方向リストで繋げて行ってる。同じラベルがあるか調べる時はハッシュを用いているが、面倒だったのでバイナリサーチなどは使わず線形サーチしてる(ぐげぇ)。なのでラベル数が増えると比例してアセンブル速度が遅くなる。……が、現在のマシンでは気にするレベルではない。プログラマとしては気にしないといけない部分ではあるけれどもw さすがに仕事では手を抜かずに作っているのでご安心を(違

 

ラベル登録時に値を決定したいので、命令長を確定しつつ解析を進めるようになっている。
ただし6301CPUの場合はダイレクトアドレッシング(1バイトの絶対値)とエクステンドアドレッシング(2バイトの絶対値)を区別する方法が、実際の数値が$00〜$FFに収まっているかどうかしか判定基準がない。このため参照時にundefinedな場合は自動的にエクステンドとして処理をしている。ここは書き手に依存している部分。

 

AIM/EOM/OIM/TIMだけは記述が特殊なので、内部でも特別扱いしている。この4つの命令だけがオペランドを3つ持つ。ただし命令長はどのアドレッシングでも3バイトなので、ニーモニックの解析のみで命令長は決定される。

 

2パス目の処理を簡単にするため、ニーモニック解析は1パス目で行っている。疑似命令(ORGやDB/DS/ENDなど)も中間形式に変換してメモリに保管。

 

2パス目

2パス目はコードジェネレートが主な仕事。
1パス目でだいぶ解析が進んでいるので、その結果(1パス目で保存している)を利用しながらコードを決めていく。
2パス目で構文チェックもしているが、手抜き感満載。
おそらく他の人が使ったら度肝を抜くレベルw

 

命令解析については、記述されたアドレッシングを参考にしている。
この部分は…作るアセンブラの対応するCPUによって変えていくとラクな気がする。
ちなみにZ80アセンブラを作った時は、ニーモニック主体で決めていった(LD命令、INC/ANDなどの区分から)。
6301はアドレッシングで場合わけして行った方が圧倒的に解析がラクだったのでそうしただけ。
ちなみに過去に作った6809アセンブラでも、アドレッシング主体で解析した経験アリ。

 

6301はベースとなるマシン語コードから、アドレッシングの違いにより+$10ずつズレていく。
例えば「LDA」を例にしてみると、

コード アドレッシング  表記
$86  イミディエイト  LDA #$12
$96  ダイレクト    LDA $34
$A6  インデックス   LDA $56,X
$B6  エクステンド   LDA $1234

という感じだ。

つまりアドレッシングを解析すれば命令コードは自動的に決まる。命令コードが決まれば命令長も確定する。コード生成の直前までパス1で処理しているので、パス2ではその結果に基づいてコード+オペランドを出力しているだけだ。

 

パス2の段階で未定義ラベルは存在しないはず。
存在しないとなるとそれは本当に未定義ラベルなのでエラーを出力して停止させる。

 

疑似命令(ORGやDB/DW/DS/ENDなど)は最低限しか用意していない。
1バイトのデータ列はDB、2バイトのデータ列はDWで記述し、エリア確保はDSといった単純なモノ。

 

db $01,'A',"Hello",CR,(($10+NUM)*8),$0
dw $01,'A',"Hello",CR,(($10+NUM)*8),$0

 

上の書き方の場合、先頭の$01はdbの場合は1バイト、dwの場合は2バイトで格納される。こういうのを簡単に処理するために、1つのデータを得る処理と、それを何バイトのメモリに格納するのかを別に処理している。

[,]と[,]の間にある{式}を正しく処理してやる事で値が得られる。得られないとしたら、それは未定義シンボルが使われている。

 

最終的に出来上がったコードは、バイナリファイルとして出力するか、そのままC言語でinclude出来るテキストで出力するか、どちらかしかしてない。インテルHEX形式とかそういう豪華な出力仕様は付けていないのだ。

 

パス2自体の作業はとても単純で、ソースコードとしても150行もない。
ただただ機械的にコードを出力しているだけだ。

こんな感じの手抜きアセンブラであり、開発時間も合計で12時間にも満たないと思う。

 

書いてて「この文章の需要はあるんだろうか」と大きな疑問が…(^^;;

まぁ記録にもなるからいいかw

 

ではまた次回!(^-^)ノ