SNOBOL言語
2018年4月、歴史的に貴重なDEC社 TOPS-20のSNOBOL言語で8080アセンブラを作成しました。私は大学生のとき(1977年)に、DEC社 SYSTEM-20 (TOPS-20)でSNOBOL言語と出会ってその素晴らしさに感動しました。
KLH10エミュレーター(Panda TOPS-20 distribution)をインストールして、SNOBOL言語が使える環境が整いました。
SNOBOL言語で8080アセンブラ
大学4回生のときに、BASIC風のインタプリターをSNOBOL言語で作成しました。残念ながらそのSNOBOLソースコードをいつの間にか捨ててしまったようで、その資料は手元にありません。ただSNOBOL言語の素晴らしさだけが記憶に残っています。
そこで、SNOBOL言語の醍醐味(だいごみ)を再び体験しようと考えて、今回はインテル8080のアセンブラ(ASM80.SNO)を作成しました。
8080プロセッサーも懐かしくわが青春のひとコマです。1978年ごろ、TK-80マイコン・ボードで趣味のプログラミングをしていました。
このブログでは、ASM80.SNOの設計編を書きます。
SNOBOL言語による8080アセンブラをTダイアグラムで記述すると下記のようになります。
8080ソースコードは、SNOBOLで記述したASM80によって8080機械語にアセンブルします。SNOBOLは、PDP-10 KL10機械語で記述しています。KL10機械語は、x86プラットフォームでKL10 Emulatorでエミュレートします。
ASM80.SNOの仕様
TK-80マイコン・ボードはもう手元になく、8080が動作するボードの手持ちもありません。そこで、ASM80.SNOは実用レベルというよりはトイプログラム(Toy)レベルとします。
コメントは、セミコロン(;)から始まり行末までです。英大文字と英子文字の区別はなく、内部的にすべて英大文字で扱います。
ラベルは、英数字で構成してコロン(:)で区切ります。
疑似命令は、以下をサポートします。
機能 | 疑似命令 | 動作 |
---|---|---|
定数定義 | ラベル名 EQU 数値 | ラベル名を数値で定義 |
アドレス設定 | ORG アドレス | アドレスを設定 |
1バイトの定数定義 | DB バイト定数 | 数値 or ラベル名でバイトを定義 |
2バイトの定数定義 | DW ワード定数 | 数値 or ラベル名でワードを定義 |
領域定義 | DS 領域数 | 数値 or ラベル名で領域を定義 |
ASM80.SNOの設計
8080命令コードの生成
8080機械語を扱うときは16進数を使うことが多く、よく使う命令の機械語は覚えていたものです。
(JMPは’C3’、CALLは’CD’、RETは’C9’など)
8080機械語を説明したドキュメントの中には8進数で表記したものもあります。その理由は、8080機械語のビットフィールドは8進数で表記した方がわかりやすいからです。
- 命令「MOV Rd,Rs」は、レジスタRd←レジスタRsレジスタ間の転送命令です。
- 機械語は2進数表記で「01 ddd sss」(8進数表記 1DS)となります。dddとsssの各3ビットはレジスタ(B,C,D,E,H,L,M,A)を0~7で示します。
- 例えば「MOV A,C」は ddd=A=7、sss=C=1 ですので、「01 111 001(171)」となります。
- 命令「INX Rp」は、ペアレジスタRpのインクリメント命令です。
- 機械語は2進数表記で「00 pp0 011」(8進数表記 0P3)となります。pp0の3ビットは0~6の偶数(最下位ビットは0)でレジスタ(B,D,H,SP)を示します。
- 例えば「INX H」は pp0=H=4、「00 100 011(043)」となります。
- 命令「LDAX Rp」は、ペアレジスタRpのレジスタ間接ロード命令です。
- 機械語は2進数表記で「00 qq1 010」(8進数表記 0Q2)となります。qq1の3ビットは0~3の奇数(最下位ビットは1)でレジスタ(B,D)を示します。
- 例えば「LDAX D」は qq1=D=3、「00 011 010(032)」となります。
- 命令「STAX Rp」は、ペアレジスタRpのレジスタ間接セーブ命令です。
- 機械語は2進数表記で「00 pp0 010」(8進数表記 0P2)となります。pp0の3ビットは0~2の偶数(最下位ビットは0)でレジスタ(B,D)を示します。
- 例えば「STAX D」は pp0=D=2、「00 010 010(022)」となります。
SNOBOLでASM80を実装するために、命令コードテーブルに機械語の8進数表記を格納します。そして、レジスタ指定によりレジスタ部分(D,S)を置換して命令コードを生成します。
また、8080機械語は命令の種別によって1~3バイトの命令長となりますので、命令コードテーブルに命令長も格納します。
OPCODE<“MOV” > = “1DS+1”
OPCODE<“MVI” > = “0D6+2”
8080命令のオペランド部
8080の命令によりオペランド部を解析します。
形式 | 命令 | オペランド部 |
---|---|---|
RM_RM | MOV | Rd,Rs |
RM_IMM | MVI | Rd,Imd8 |
RM | INR/DCR ADD/ADC/SUB/SBB ANA/XRA/ORA/CMP |
R |
IMM | ADI/ACI/SUI/SBI ANI/XRI/ORI/CPI |
Imd8 |
PORT | IN /OUT | Port |
NON | HLT/EI /DI /NOP RLC/RRC/RAL/RAR RET RC /RNC/RZ /RNZ RP /RM /RPE/RPO XCHG/XTHL/SPHL/PCHL CMA/STC/CMC/DAA |
|
ADR | JMP JC /JNC/JZ /JNZ JP /JM /JPE/JPO CALL CC /CNC/CZ /CNZ CP /CM /CPE/CPO LDA /STA/LHLD/SHLD |
Adr |
VEC | RST | Vect |
BDHA | PUSH/POP | Rp |
BD | LDAX/STAX | Rp |
BDHS | DAD/INX/DCX | Rp |
BDHS_I | LXI | Rp,Imd16 |
ASM80.SNOのプログラム構造図
SNOBOL言語による8080アセンブラ(ASM80.SNO)は、2パス方式で処理します。
- PASS1 : ラベル登録、エラーチェック
- PASS2 : ラベル参照、エラーチェック、オブジェクトコード生成
全体のプログラム構造
プログラムのロジックはフローチャートではなく、構造化フローチャートのPAD図で書いた方が理解しやすいです。
ASM80.SNOのプログラム構造をPAD図で示します。
2パス方式なので、PASSで2回ループします。
ひとつのパスでは、PC←0に設定しINPUT_DEVからソースコードのラインを入力してEOFになるまで繰り返します。
ひとつのラインでは、次の処理を行います。
- エラーリセット
- 命令長 OPLEN←0
- INPUT_DEVから入力
- 英大文字に変換
- コメント除去
- ラベルの形式なら、ラベル登録
- 疑似命令形式なら、疑似命令処理へ分岐
- 8080命令の処理なら、命令コードと命令長を取得し、オペランド処理へ分岐
- PASS=2なら、リスト出力とエラー出力
- PC←PC+命令長
ORG 疑似命令
ORG アドレス ; アドレス設定
GETIMM()で、”ORG”の右側のアドレスを取得しPCに格納してアドレスの設定を行います。
DB 疑似命令
DB バイト定数(数値 or ラベル名) ; 1バイトの定数定義
- GETIMM()で、”DB”の右側のバイト定数(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
- 命令長 OPLEN←1として、オブジェクトの1バイト目に定数IMMを設定します。
DW 疑似命令
DW ワード定数(数値 or ラベル名) ; 2バイトの定数定義
- GETIMM()で、”DW”の右側のワード定数(AD)を取得します。
- 命令長 OPLEN←2として、オブジェクトの1バイト目に定数ADの下位バイト、2バイト目にADの上位バイトを設定します。
DS 疑似命令
DS 領域数(数値 or ラベル名) ; 領域定義
- GETIMM()で、”DS”の右側の領域数(IMM)を取得します。
- 命令長 OPLEN←IMMとして領域を確保します。
- オブジェクトの1~3バイト目には、”??”を設定します。
EQU 疑似命令
ラベル名 EQU 数値 ; 定数定義
- ラベルの形式でなければ、LABELエラーとします。
- GETIMM()で、”EQU”の右側の数値(IMM)を取得します。
- ラベルをIMMでラベル登録します。
命令 オペランド BD形式
LDAX/STAX命令は、オペランドにレジスタ(B,D)を指定します。
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,D)をコード(0,2)に変換します。
- 命令コード OPCのレジスタ部分(P Q)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド BDHS形式
DAD/INX/DCX命令は、オペランドにレジスタ(B,D,H,SP)を指定します。
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,D,H,SP)をコード(0,2,4,6)に変換します。
- 命令コード OPCのレジスタ部分(P Q)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド BDHA形式
PUSH/POP命令は、オペランドにレジスタ(B,D,H,PSW)を指定します。
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,D,H,PSW)をコード(0,2,4,6)に変換します。
- 命令コード OPCのレジスタ部分(R)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド BDHS_I形式
LXI命令は、オペランドにレジスタ(B,D,H,PSW)とワード定数 を指定します。
- GETIMM()で、カンマ”,”の右側のワード定数(ADR)を取得します。
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,D,H,PSW)をコード(0,2,4,6)に変換します。
- 命令コード OPCのレジスタ部分(P)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
- オブジェクトの2バイト目にADRの下位バイト、3バイト目にADRの上位バイトを設定します。
命令 オペランド RM_RM形式
MOV命令は、オペランドに転送先レジスタと転送元レジスタを指定します。
転送先 | 転送元 |
---|---|
A,B,C,D,E,H,L | A,B,C,D,E,H,L |
A,B,C,D,E,H,L | M |
M | A,B,C,D,E,H,L |
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
- 命令コード OPCのレジスタ部分(D S)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド RM_IMM形式
MVI命令は、オペランドにレジスタ(B,C,D,E,H,L,M,A)とバイト定数を指定します。
- 該当レジスタ以外のときは、レジスタエラーとします。
- GETIMM()で、カンマ”,”の右側のバイト定数(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
- レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
- 命令コード OPCのレジスタ部分(D)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
- オブジェクトの2バイト目にIMMを設定します。
命令 オペランド RM形式
下記命令は、オペランドにレジスタ(B,C,D,E,H,L,M,A)とバイト定数を指定します。
INR/DCR
ADD/ADC/SUB/SBB/ANA/XRA/ORA/CMP
- 該当レジスタ以外のときは、レジスタエラーとします。
- レジスタ名(B,C,D,E,H,L,M,A)をコード(0,1,2,3,4,5,6,7)に変換します。
- 命令コード OPCのレジスタ部分(D)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド IMM形式
下記命令は、オペランドにバイト定数を指定します。
ADI/ACI/SUI/SBI/ANI/XRI/ORI/CPI
- GETIMM()で、バイト定数(IMM)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
- オブジェクトの1バイト目に命令コード OPCを設定します。
- オブジェクトの2バイト目にバイト定数 IMMを設定します。
命令 オペランド PORT形式
下記命令は、オペランドにバイト定数を指定します。
IN/OUT
- GETIMM()で、バイト定数(PORT)を取得します。1バイトの定数定義なので、255を超える場合はIMMエラーとします。
- オブジェクトの1バイト目に命令コード OPCを設定します。
- オブジェクトの2バイト目にバイト定数 PORTを設定します。
命令 オペランド VEC形式
RST命令は、オペランドにベクトルを指定します。
- GETIMM()で、定数(VEC)を取得します。0~7の定数定義なので、7を超える場合はVECエラーとします。
- ベクトル(0,1,2,3,4,5,6,7)をコード(0,1,2,3,4,5,6,7)に変換します。
- 命令コード OPCのベクトル部分(V)をコードで置換します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
命令 オペランド ADR形式
下記命令は、オペランドにアドレスを指定します。
JMP /JC /JNC/JZ /JNZ/JP /JM /JPE/JPO
CALL/CC /CNC/CZ /CNZ/CP /CM /CPE/CPO
LDA /STA/LHLD/SHLD
- GETIMM()で、ワード定数(ADR)を取得します。
- オブジェクトの1バイト目に命令コード OPCを設定します。
- オブジェクトの2バイト目にADRの下位バイト、3バイト目にADRの上位バイトを設定します。
命令 オペランド NON形式
下記命令は、オペランドがありません。
HLT/RLC/RRC/RAL/RAR
RET/RC /RNC/RZ /RNZ/RP /RM /RPE/RPO
XCHG/XTHL/SPHL/PCHL
CMA/STC/CMC/DAA/EI /DI /NOP
- オブジェクトの1バイト目に命令コード OPCを設定します。
サブルーチン ラベル登録 ADDLTBL()
ラベルテーブルのLABELにVALを登録します。
ADDLTBL(LABEL,VAL)
- LABELが登録済みの場合は、二重登録エラーなので FRETURNします。
- パス別にふたつのラベルテーブルを使い分けます。
- PASS1 : TBL_LAB1<LABEL>にVALを登録
- PASS2 : TBL_LAB2<LABEL>にVALを登録
サブルーチン ラベル参照 REFLTBL()
ラベルテーブルのLABELを参照します。
REFLTBL(LABEL)
- PASS=1のとき、LABELが未登録なら “0”を返します。
- TBL_LAB1<LABEL>を参照します。パス1用のラベルテーブルを参照します。
サブルーチン 直値参照 GETIMM()
文字列 STR から直値を参照します。
GETIMM(STR)
直値は、以下の形式とします。
- 16進数 : 0~Fの組み合わせ に続く “H”
- 2進数 : 0~1の組み合わせ に続く “B”
- 10進数 : 0~9の組み合わせ
- ラベル : 英字、数字、アンダーバー(“_”)の組み合わせ
ラベルの場合は、ラベルテーブルを参照します。
- 直値のパターンに一致しない場合は、パターン不一致エラーで FRETURNします。
- 直値のパターンに一致した場合は、一致部分のIMMとHB識別を設定します。HB識別(”H” or “B” or “”)によりBASE(16 or 2 or 10)に変換します。
- 一致部分 IMMをBASE進数の数値に変換します。
- 数値変換が正常なら、数値を戻り値にします。
- 数値変換が異常なら、ラベルとみなしてIMMでラベル参照してSTRに設定して直値のパターンチェックを繰り返します。
SNOBOLによる8080アセンブラ設計完了
SNOBOL言語による8080アセンブラの設計が完了しました。細かいエラーチェックなど省略していますが、使用する分には問題ないと考えます。SNOBOLで書いたソースコードは、次回に書きます。
TOPS-20に関するブログ
歴史的に貴重なDEC社 TOPS-20を使うためにKLH10エミュレーターをLinuxにインストールして、TOPS-20を実行することができました。
TOPS-20に関するブログを以下に示します。
ピンバック: DEC社 TOPS-20のSNOBOL言語で8080アセンブラを作成(コード編) | ある計算機屋さんの手帳
ピンバック: DEC社 TOPS-20のSNOBOL言語で8080アセンブラを作成(テスト編) | ある計算機屋さんの手帳