マシン語で配列変数を読み書きしよう

(New:1997/5/28)
BASICの配列変数をマシン語側で読み書きする方法について考えてみましょう。

ここでは整数型1次元配列について考えていくことにします。
※整数型以外だととってもややこしい(?)ので省略します。 2次元以上の配列の場合は忘れてしまいました。 すんません(^_^;)。

整数型1次元配列の内容は、メモリには以下のようにならんでいます。

AD      HA(0)の内容(下位 1 バイト)
AD+1    HA(0)の内容(上位 1 バイト)
AD+2    HA(1)の内容(下位 1 バイト)
AD+3    HA(1)の内容(上位 1 バイト)
AD+4    HA(2)の内容(下位 1 バイト)
AD+5    HA(2)の内容(上位 1 バイト)
......
ここでADは配列の先頭アドレス(= HA(0)の中身のある場所)、HA(n)は配列変数です。 つまり、HA(n)の内容はAD + (n × 2)にその下位1バイトが、AD + (n × 2) +1に上位1バイトが入るわけです。
このように、配列の内容は添字の順にメモリにならんでいます。 配列の先頭アドレス(上の例ではAD)が分かれば、マシン語から配列のデータを読み書きできそうです。

そこで、配列の先頭アドレスをマシン語に渡す方法を考えてみましょう。 ユーザーが決めたアドレスに先頭アドレスを書き込む方法も考えられますが、ここはひとつスマートにUSR関数の引数として渡す方法を考えてみましょう。

BASICにはVARPTR(variable pointer:「バリアブルポインター」と読みます)という関数が用意されています。 これは、指定した変数の内容が収められているアドレスを返すものです。例えば、
PRINT VARPTR(P%)
とすると、変数P%のアドレスがモニタに出力されます(マイナス値が出てくる場合は、それを16進化するか、65536を加えたものがアドレスとなります)。

さて、配列の先頭アドレスもやっぱりVARPTR関数で求めることができます。
VARPTR(HA(0))
という感じで。
ただ、それをUSR関数の引数として渡すには注意が必要です。 以下、サンプルプログラムをば。


マシン語プログラムに配列の先頭アドレスを渡すサンプルプログラム(誤った例)
※BASICプログラム。 一部機械語含む。 MSX1(RAM 16KB以上)で実行可能。
10  CLEAR200,&HCFFF
20  DEFINT  A−Z
30  RESTORE1000:AD=&HD000
40  READ  D$:IF  D$=”*”  THEN  ELSE
  POKE  AD,VAL(”&H”+D$):AD=AD+1:
GOTO40’マシンご  プログラム  スタンバイ
50  DEFUSR=&HD000
60  DIM  A(15)
70  A(0)=2480
80  B=VARPTR(A(0)):C=USR(B)
90  PRINT  C
100  END
1000  DATA  23,23,5E,23,56,2B,EB
,ED,A0,ED,A0,C9,*
10行でマシン語領域を確保しています。
20行でこのプログラムで使う変数はすべて、通常は整数型変数としています。
30行、40行でマシン語プログラムを用意しています。
50行でマシン語コールアドレスを指定しています。
60行で配列変数を定義しています。 大きさは適当です。
70行から90 行については後述します(ここが肝心なところ)。
1000行はマシン語プログラムデータです。
マシン語部分はBASICからUSR関数でコールされると、引数で渡されたアドレスの内容(2バイト数値)を返り値として返します。 例えば、
I=USR(5)
とすると、変数Iには5番地の内容+6番地の内容×256が入ります。 ソースコードは以下のようになっています。
	ORG	0D000H
	INC	HL
	INC	HL
	LD	E,(HL)
	INC	HL
	LD	D,(HL)
	DEC	HL
	EX	DE,HL
	LDI
	LDI
	RET
END
マシン語でUSR関数の引数の読み方や返り値の書き方が分からない人のために補足説明をします。 まずUSR関数で呼ばれた時、HLレジスタには引数のあるアドレスが入っているので(HL+2に下位1バイト、HL+3に上位1バイトが入っています)、そこをチェックしてやればいいです。 返り値は引数の入っていた場所に返したい値を書き込んでやればいいです。
※いずれも整数型変数の場合の話です。 他の型では引数の読み方、返り値の書き方は異なります。
このプログラムの要は80行にあります。 80行をもう一度見てみましょう。
80  B=VARPTR(A(0)):C=USR(B)
70行でA(0)に2480という値を入れています。 80行ではまず、変数Bに配列A(n)の先頭アドレス、つまりA(0)の内容があるアドレスを代入しています。 次に、このBをマシン語プログラムの引数としてUSR関数を実行しています。 返り値CにはB番地の内容が入ります。
さて、これで変数CにはA(0)の内容、つまり2480が入るように見えます。 90行では変数Cの内容をモニタに出力します。 しかし、実行結果は…モニタには2480以外の数値が出力されてしまいます(非常に運が良ければ2480ということもあるかもしれませんが)、なんでじゃあ!?

どうやら、80行のUSR関数の引数として渡した値は、配列A(n)の先頭アドレス(= A(0)のアドレス)ではなく、全然関係のない場所の中身が(変数Cに)返ってきてしまったようです。
でも、引数で渡したのは確かにA(0)のアドレスだったはず、それを変数Bに入れて引数として渡し…。
実はそこに落し穴があったのです。 BASICの変数、特に配列変数は、何か命令を実行すると、その記憶場所が変わることが多いようなのです

HB-F1XDJ付属のBASIC文法書のVARPTRの所にも、変数に値を代入した地点で変数の番地が変わるということが書かれています。
サンプルプログラムの80行でも、VARPTR関数で得られた配列A(n)の先頭アドレスを一旦変数Bに格納し、それからUSR関数の引数として渡していますね。 このたった2つの命令の間で、配列A(n)の記憶場所が変わってしまったのです。 ですから、変数Bを引数として USR関数を使っても、その場所(変数Bが示すアドレス)には既に配列A(n)はなく、全然関係のない値を返してしまったというわけです。 BASICってせわしないやつだねぇ。

サンプルプログラムの80行を次のように変えて実行すると、今度はモニタにちゃんと「2480」と出力されます。

80  C=USR(VARPTR(A(0)))
VARPTR関数でアドレスを得てすぐに使ってしまおうというわけです。
このように、配列変数のアドレスをUSR関数の引数としてマシン語に渡すには、変数を挟まず直接VARPTR関数を埋め込んでしまった方がいいでしょう。
あと、マシン語側では配列の大きさに注意しましょう。 BASICで定義した以上の範囲で読み書きするとまずいでしょうし。
MSXの適当手帳 MSX駅 西山駅