BASICからマシン語プログラムを呼ぶ
ちょっと趣旨を変更して
いままでの記事では男気あふれるマシン語オンリーの記事でした。
今回はちょっと趣旨を変更して、BASICとマシン語を連携させてみようと思います。少し長めの記事ですが最後まで読んでみてください。
BASICがどのようにメモリ上に配置されるのか知る
BASICはインタプリタ言語です。BASICでプログラムを書くとメモリ上に中間言語(BASICでもマシン語でもないコード)としてプログラムのコードが展開されます。例えばRETURN文であれば、8EHとかっていう中間コードになってメモリ上に格納されます。
BASICのプログラムの中間言語は以下の形式で8000Hから格納されていきます。
上図にあるとおり8000Hはテキスト開始コードになっていて固定で00Hがはいっています。つまり実際のコードは8001Hからということになりますね。
8001Hからプログラムの行を中間コードとして解釈して格納されている形式です。
リンクポインタには次のBASIC行のポインタが格納されています。次の行を見たい場合はこのポインタの指し示すアドレスを見れば良い。という形ですね。行番号(2バイト)にはそのものずばりのBASIC行の行番号が格納されます。
その次のアドレスからBASIC行を中間コードに変換した結果が格納されます。図中のテキストとかかれている部分が格納先になります。
次のようなイメージです。(あくまでもイメージなので完全にこうなるというわけではありません。ご容赦ください)
(BASIC)
1000 SCREEN 1
1001 PRINT "ABC"
1002 END
(中間コード)
8000: 00
8001: 08 80 ← リンクポインタ(次の行は8008H)
8003: E8 03 ← 行番号(1000行)
8005: C5 01 ← SCREEN 1
8007: 00 ← 行の終端は必ず00Hが入る
8008: 11 80 ← リンクポインタ(次の行は8011H)
800A: E9 03 ← 行番号(1001行)
800C: 91 41 42 43 ← PRINT "ABC"
8010: 00
8011: 17 80 ← リンクポインタ(次の行は8017H)
0013: EA 03 ← 行番号(1002行)
8015: 81 ← END
8016: 00 ← 行の終端は必ず00Hが入る
8017: 00 00 プログラムの終端は 0000H が入る
インタプリタはこの中間コードをもとにして実際のマシン語に逐次翻訳しながらプログラムを実行していくのです。なので遅い。ほんとに遅い。
なので、マシン語はどうしても切っても切れない間柄なのです。
BASICでマシン語プログラムを作成する
BASICではマシン語プログラムをいくつかの方法で作成することができます。
ひとつめはBLOADなどで他の媒体(ディスクなど)からマシン語を読み込む方法。
ふたつめはBASICプログラム内でマシン語プログラムを作成する方法です。
MSX1ではFDDがついていない機種も多いため今回の記事ではふたつめのBASICプログラム内部でマシン語プログラムを作成する方法を解説します。
マシン語プログラムをBASICから呼べるようにするための呪文
マシン語プログラムをBASICから呼べるようにするためには以下のように書きます。
1001 REM ASM Function Definition.
1002 DEF USR(0)=&HD000
1003 REM ASM Function call.
1004 A=USR(0)
DEF USR(0)=&HD000
と書くことで&HD000から始まるマシン語プログラムをUSRという名前で定義します。
A=USR(0)
と書くことでUSRという名前のマシン語プログラムを呼び出します。(CALL)
マシン語プログラム側は必ずRETで終わらなければいけません。USRのうしろの(0)はマシン語に渡すパラメータ(引数)です。今回のサンプルでは引数無しですが必ず指定する必要があるみたいなので0と書いてます。
マシン語プログラム側でRETが行われるとBASICコードに戻ってきます。
マシン語には引数として整数や文字列、実数などを渡せたりマシン語プログラム側から戻り値を受け取ることも可能ですが、当記事では引数や戻り値については説明を省略します。長くなるので(汗)
とりあえずコードを見てみる?
今回もサンプルコードを用意しています。なので、とりあえずサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample018
今回のプログラムは以下のふとしたことから作成を思いつきました。
・BASICだけでGRAPHIC2のPCGデータを作成しようとするとものすごく処理に時間がかかるよねえ・・速くしたいよねえ・・
・PCGデータの作成手法としてBSAVEとかBLOADとかを使ってバイナリデータを読み込んでVRAMに直接書き込んでしまうという手法もあるがそれだとPCGのパターンデータを修正するたびにBSAVEしなおさなければならない手間があるよねえ・・面倒んだよねえ・・
・(ピコーん!ひらめいた!)BASICでパターンデータやカラーデータを記述し、その内容をマシン語処理で読み込んでPCGデータを作成してみてはどうだろう?
と、いうことで以下の処理に分かれています。
1. BASIC本体(sample018.bas)
BASICの本体であり、マシン語プログラム自体も自分で作成する。
PCGのパターンデータやカラーデータについては特定の行番号に記述している。マシン語を呼び出してPCGを作成し、その結果を表示するだけとなっている。
2. マシン語(&HD000が開始アドレス)
BASICの中ではDATA文としてアセンブルされた結果が記述されている。マシン語の内容はBASICプログラムの62000行目以降に記述されているREM文の内容をPCG作成用のデータとして解釈して各文字のパターンとカラーデータを生成します。どうやって62000行目を見つけているかについては前述した「8000H以降にBASICのソースコードが展開される」という仕様を利用しています。
アセンブラのソースコードは別途、sample018.asmを参照してください。
BASIC部分の説明
BASIC部分は以下のようになっています。
10000 SCREEN1:COLOR15,1,1:KEYOFF:CLEAR 300,&HD000
10001 REM ==============================================
10002 REM An experiment to have PCGs created at high speed in
10003 REM machine language by using comment sentences in
10004 REM BASIC programs as PCG creation data.
10005 REM We have not yet compared whether it is really fast or not. :)
10006 REM
10007 REM NOT USED "BLOAD" VERSION.
10008 REM
10009 REM Author: SAILORMAN STUDIO
10010 REM ===============================================
10011 ST=TIME
10012 PRINT "LOAD ASM ROUTINE."
10013 GOSUB 10025
10014 DEF USR=&HD000
10016 GOSUB 62000
10018 A=USR(0)
10019 ET=TIME
10020 CLS:FOR I=33 TO 90:PRINT CHR$(I);:NEXT I
10021 PRINT:PRINT "START ";ST;": END ";ET:END
10022 REM ===============================================
10023 REM GENERATE ASM ROUTINE.
10024 REM ===============================================
10025 FOR I=0 TO 21:READ A$
10026 FOR J=0 TO 15
10027 AD=&HD000+(I*16)+J
10028 VL=VAL("&H"+MID$(A$,J*2+1,2))
10029 POKE AD, VL
10030 NEXT J
10031 NEXT I
10032 RETURN
11000 REM ===================================
11001 REM MACHINE LANGUAGE DATA
11002 REM
11003 REM original source : sample018.asm
11004 REM ===================================
12001 DATA "CD7E00C307D0C9CDD0D0CDA1D02A00DB"
12002 DATA "CD80D07EFE8ECA7DD0237E3204DB2A02"
12003 DATA "DBCD80D023CD1CD1CD95D0CD8BD0CD95"
12004 DATA "D021000819545DCD8BD0CD95D0210010"
12005 DATA "19545DCD8BD02A02DBCD80D023CD1CD1"
12006 DATA "CD95D021002019545DCD8BD0CD95D021"
12007 DATA "002819545DCD8BD0CD95D02100301954"
12008 DATA "5DCD8BD02A02DB2200DBC30DD0C306D0"
12009 DATA "5E235623ED5302DB2323C92105DB0108"
12010 DATA "00CD5C00C93A04DB671E08CD0BD1545D"
12011 DATA "C92101802200DB7E5F237E57626B220D"
12012 DATA "DBED5B0DDB2A00DB23237EFE30C2CAD0"
12013 DATA "237EFEF2C2CAD0C3CFD0626BC3A4D0C9"
12014 DATA "2100001100D3010008CD59002100D311"
12015 DATA "0008010008CD5C002100D31100100100"
12016 DATA "08CD5C002100200100183EF1CD560021"
12017 DATA "00180100033E20CD5600C9C5D516002E"
12018 DATA "0006082930011910FAD1C1C9F5C51105"
12019 DATA "DB06080E007EFE41D230D1D630C332D1"
12020 DATA "D637CB27CB27CB27CB274F237EFE41D2"
12021 DATA "47D1D630C349D1D637B112231310D6C1"
12022 DATA "F1C90000000000000000000000000000"
60000 REM ===============================================
60001 REM CHARACTER PATTERN AND COLOR DATA
60002 REM ===============================================
62000 REM!
62001 REM3838383800003800
62002 REMF1F1F1F1F1717151
62003 REM"
62004 REM6C6C6C0000000000
62005 REMF1F1F1F1F1717151
(中略)
62999 RETURN
処理としては、まず10025行目を呼び出します。10025行から10032行目まででマシン語のプログラムをメモリに展開しています。処理が終わると10014行目に戻ってきます。
10029行目にPOKE AD, VLという記述がありますが、POKE命令はメモリに値を書き込むBASICの命令です。ADにアドレス、VLに書き込む値がセットされた状態で呼び出すと指定したアドレスに指定した値が書き込まれます。書き込んだ値はMSXをリセットするまでメモリ上にずっと残った状態になります。
10014行目で、上記処理で書き込まれたマシン語プログラムをBASICで使えるようにUSRとして定義しています。USRのうしろに番号をつけないとUSR0と同じになります。
10016行目で、62000行目を呼び出しています。62000行目以降はずっとREMです。REMはBASICでの注釈行のため特段処理は行われません。
62999行目で、10018行目に戻ってきます。
10019行目でマシン語プログラムUSR0を呼び出しています。USR(0)の括弧内のゼロは無視してください。マシン語プログラムに渡す引数になってますが、今回はマシン語ではその引数を使っていません。A=USR()だとBASICでSyntaxErrorになってしまいます・・。不思議。
62000行目が必ずBASICに存在していなければなりません。
PCGデータがない場合は
62000 RETURN
と必ず書きましょう。そうしないとループします。
マシン語のキモ部分(BASICの特定行を探すロジック)
キモというか、肝というか、キモいというか・・。
マシン語のキモ部分は8000H以降のBASIC文から62000(F230H)という行番号がついてる行を見つけるところです。
GetAddressBASICCHRDATAサブルーチンでそれをやっています。
ここまでの当記事の愛読者であればもうASMのソースはおおよそ読めるようになっていると思うのでそれ以外の部分や詳細は割愛します。
(sample018.asm 抜粋)
;--------------------------------------------
; SUB-ROUTINE: GetAddressBASICCHRDATA
; パターンジェネレータデータとカラーデータが
; 格納されているBASICのREM行の先頭位置を探す
; HLレジスタにそのBASIC行のテキストデータの
; アドレスがセットされて返却される
;--------------------------------------------
GetAddressBASICCHRDATA:
ld hl, $8001 ; BASICの先頭は8001H
GetAddressBASICCHRDATALoop:
ld (WK_HLREGBACK), hl
ld a, (hl)
ld e, a
inc hl
ld a, (hl)
ld d, a
ld h, d
ld l, e
ld (WK_REMLINE_WORK), hl
ld de, (WK_REMLINE_WORK) ; リンクポインタをDEレジスタにセット
ld hl, (WK_HLREGBACK)
inc hl ; HLレジスタを2つ進める
inc hl ;
ld a, (hl)
cp $30
jp nz, GetAddressBASICCHARDATALoopNextData
inc hl
ld a, (hl)
cp $F2
jp nz, GetAddressBASICCHARDATALoopNextData
jp GetAddressBASICCHARDATALoopEnd
GetAddressBASICCHARDATALoopNextData:
ld h, d
ld l, e
jp GetAddressBASICCHRDATALoop
GetAddressBASICCHARDATALoopEnd:
; 行番号62000を検知
ret
アセンブルされたデータをBASICのDATA文に変換する方法
z80asmコマンドでアセンブルされたデータはバイナリデータです。
そのままだとBASIC文にどうやって書けば良いのかわかりませんよね。
なので、今回は z8make というスクリプトも用意しました。LinuxやmacOS用なのでWindowsの方はUbuntuなりをインストールして試してみてください。
z8make sample018 256
とスクリプトを実行すると、256バイトのデータが以下のように出力されます。
% ./z8make sample018 256
2+0 records in
2+0 records out
512 bytes transferred in 0.000071 secs (7211268 bytes/sec)
BASIC CODE is below
REM ===================================
REM MACHINE LANGUAGE DATA
REM
REM original source : sample018.asm
REM ===================================
DATA "CD7E00C307D0C9CDD0D0CDA1D02A00DB"
DATA "CD80D07EFE8ECA7DD0237E3204DB2A02"
DATA "DBCD80D023CD1CD1CD95D0CD8BD0CD95"
DATA "D021000819545DCD8BD0CD95D0210010"
DATA "19545DCD8BD02A02DBCD80D023CD1CD1"
DATA "CD95D021002019545DCD8BD0CD95D021"
DATA "002819545DCD8BD0CD95D02100301954"
DATA "5DCD8BD02A02DB2200DBC30DD0C306D0"
DATA "5E235623ED5302DB2323C92105DB0108"
DATA "00CD5C00C93A04DB671E08CD0BD1545D"
DATA "C92101802200DB7E5F237E57626B220D"
DATA "DBED5B0DDB2A00DB23237EFE30C2CAD0"
DATA "237EFEF2C2CAD0C3CFD0626BC3A4D0C9"
DATA "2100001100D3010008CD59002100D311"
DATA "0008010008CD5C002100D31100100100"
DATA "08CD5C002100200100183EF1CD560021"
DATA "00180100033E20CD5600C9C5D516002E"
DATA "0006082930011910FAD1C1C9F5C51105"
DATA "DB06080E007EFE41D230D1D630C332D1"
DATA "D637CB27CB27CB27CB274F237EFE41D2"
DATA "47D1D630C349D1D637B112231310D6C1"
DATA "F1C90000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
DATA "00000000000000000000000000000000"
あとはこの出力された結果のテキストの"BASIC CODE is below"の下の行を加工してペタペタとDATA文にすればOK。もっと楽な方法があるはずなんだけどよくわからなかったのでこうしています(汗)
実行してみよう!
それではBASICを実行してみましょう。今回はいままでとは異なり、いったんsample018.basのコードを全量WebMSXのコードボックスにコピペします。
1. WebMSXを開き、Alt+B でコードボックスを開く。
2. コピーした内容をボックスにペーストする。
OKをクリックするとつらつらとペーストした内容がWebMSXの画面に転記されています。
3. run を入力してエンター!!
しばらく以下の画面が表示されたあとで・・。
こんな結果画面が表示されます。GRAPHIC2の画面になっていることが確認できるかと思います。STARTとENDは時間です。END-START=625。TIMEは1/60秒ごとにカウントアップされるので、625/60=10秒かかった計算です。(まあ10秒のほとんどはBASIC部分なんですけどね)
で、何が便利なの??
すみません・・。思いつきで作ったので利便性はよくわかっていませんがオールベーシックよりかははるかに高速で処理できていることだけは確かです。今回は50文字ぶんくらいのPCGデータしか扱っていませんが処理対象となる文字数が多ければ多いほどそのスピードを享受できるようになるかと。
それと、以下のようなことも実現可能です。
・小文字のzという文字を青い人に変えてみる。
↓こんな感じでBASIC文に追記して・・。
再度、RUN と入力してエンター!!!!
実行後、小文字のzを入力するとこんな感じで追記したBASIC文が反映されていることがわかります。
まあ、RUNしなくてもA=USR(0)って入力するだけでもいいんですけどね。以下を実行すると小文字のxも加工されます。
A=USR(0) と書いてエンターーーーーーー!!!!(無駄な気合い)
実行後、小文字のxを入力すると次のような感じです。
今回のサンプルコードでは、BASICのREM文をちょこちょこと修正してA=USR(0)を実行するだけですぐにPCGの結果を確認できるようになります。
まあ、便利っちゃあ便利(自己マン)
まとめ
今回の記事では以下のことが学習できたと思います。
・BASICからマシン語の処理を呼び出すことができる。
・DEF USRでBASICから呼び出すマシン語サブルーチンを定義できる。
・一度、DEF USRで定義するとリセットするまで何度でも使える。
・マシン語からBASICの行を参照することができる。
重い処理だけをマシン語にして、あとはプログラミングしやすいBASICでプログラムを作るというのは「MSXプログラミングの王道」です。
では、また!ノシ
おまけ
今回のサンプルコードではオールBASICのコードも用意しています。
sampla018_allbasic.bas がそれです。それを実行すると・・。
END - START = 1761。
1761 / 60 = 29秒!!(あかーん!!)
セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。