パソコン活用研究5番街(Visual Basic、Excel(VBA)、BASIC プログラミング研究)
Basicから機械語へ Part3 (Call absolute) (QBasic)
1 はじめに
今回はいよいよ機械語の登場です。ところで、なんで機械語か?
むかーしといっても10年以上前ですが、Basicである程度、ゲームなんかがくめるようになると、
だいたい機械語をリンクして、Basic+機械語プログラムを作るというのが王道でした。なんでか?
当時、1-2MHzの8bit CPUでall Basicのゲームを作ると、それはそれはのろいものでした。
スピードを追求すると、Basicでは遅い部分を機械語にして高速化するというのが、必要不可欠でした。
そしてもうひとつは、Basicではできないハードの細かい制御をやる、というのが機械語使用の理由でしょう。
(昔のall Basicプログラムの遅さを体験したいなら、N88互換Basicのページからおじさんの自作Game
をダウンロードしてお試し下さい。あなたのパソコンがpentium100なら、当時のパソコンでのスピード
は、その約半分くらいか、もっと遅いくらいです。特にFM8KAME.basが遅さを体験するのに適です)
蛇足ながら、当時のパソコンでのスピード追求の涙ぐましい努力と、そこから体得されたスピードアップ
のための技の数かずについては、旧パソコンの活用研究のページ(「パソ活研ラピュタへの道/アセンブラ」
で取り上げていますので参照して下さい。
残念ながら、Visual Basic や N88互換Basic for Win にはBasicから直接、機械語のルーチン
を呼び出す命令がありませんので、当ページはQBasic専用となります。また、N88Basic(86)には
同様の call 命令(機械語ルーチンを呼ぶ)がありますが、引数の機械語への渡しかた等の仕様が
まったく違うので、そのまま移植したのではコンピュータが暴走します。
またN88Basic(86)はじめ旧Basic では、機械語をBasicから呼ぶのに、call の他にUSR関数、
Excecなどの命令がありましたが、QBasicでは、call absolute しかありません。
手元に、QBasicの解説書が皆無に近く、当ページは筆者であるおじさんが試行錯誤で調べた結果
に基づき作成しました。もしかもすると、間違っている部分がないとも限りません。おじさん自身、この
ページの作成にあたり、10回以上パソコンをリセットさせるはめになりました。
とりあえず、ここにあげた例題プログラムは、いちおう完璧にしあげてあるつもりです。(DOS窓上で実行
する限り、システムにはなんら影響はないはずです。)
さて、いよいよ本題の機械語です。Basicに比べれば、機械語はちょっとやっかいですが、
Basicよりはるかに実行速度が速い、Basicではできない細かな制御ができる、といったプログラム
の醍醐味があなたを待っています。ともかくBasicから機械語をのぞいてみましょう。
2 call absolute
QBasicでBasicから機械語を呼び出すには、call absolute という命令を使います。書式は以下の
通りです。
call absolute ( [引数],[引数],機械語実行アドレス ) [ ]は省略可 |
引数にはBasicから機械語に渡す変数または数値を書きます。Basicから渡す変数が無い場合は 機械語実行アドレスのみ指定します。 |
実際の使用例は後で、掲載します。
3 機械語(8086)の基礎知識
8086というのは、Pentiumの祖先の16Bit CPUのことです。Pentiumで8086の命令を実行する
ことが、もちろんできます。正確な表現ではありませんが、いわゆるDOS窓というやつは、Pentiumが
8086のふりをして動いている状態です。まず、8086が基本ですので、以下8086機械語について
勉強することにしましょう。
なお、8086についての詳細については関連ページの「パソコン活研ラピュタへの道/アセンブラ」で
とりあげますので、ここでは、下記の例題プログラムを理解するのに最低限必要な知識だけをとりあげます。
(1) メモリーとアドレス
これは、大基本です。Basicから機械語へ1でやりましたので、いまひとつ不安な方は、もう一度見て下さい。
(2) レジスター
Register(レジスター) というのは、CPU内部にあるデータ記録器です。CPUの内部にある特殊なメモリと考えて
もいいかもしれません。CPU内部にあるので、メモリより高速にアクセスできます。のちのちわかると
思いますが、機械語プログラムの主要部分はレジスターの操作です。
レジスターには、AX,BX,CX,DX,SI,DIなどがあります。Basicから機械語へ1ででてきた、CS、DS
もレジスター(セグメントレジスタ)ですね。
(3) MOV
MOVはmove(移動)です。
MOV ope1(第1オペランド), ope2(第2オペランド)
はope2の内容をope1に移動(実は移動よりはコピーか代入に近い)するということです。
例えば、 MOV AX, 5
は レジスタAXに5をいれるということです。Basic流に言えば、AX=5 に近いと言えます。
MOV AX, BX と書けば、 AXにBXの値を渡す(コピー)ということです。Basic流では
AX=BX です。
BXの値はそのままなので、先に述べたように、コピーといった方が実態に近いです。
さて、MOV AX, [5000]
とすると、今度は5000をAXに代入するのではなく、メモリーの5000番地(と5001番地)のデータを
AXにいれるということになります。[ ]はその番地にあるデータをさします。
MOV AX, [BX] はBXの値そのものではなく、その番地のデータをAXに渡すということになります。
よろしいでしょうか。
*(注) 以上はDebugを使った時の書き方です。
(4) INC
INCはIncrement(増加)です。
INC AX はBasic流には AX=AX+1です。 C的に書けば AX++です。
これ以上の説明は不要でしょう。ちなみに、反対に1を減ずるDECもあります。
4 Basicから機械語を呼び出す( call absolute )
(1) 引数のない場合
最初の例は、「1」を画面に表示する機械語を呼び出すものです。Basicから機械語に渡す引数は
ないため、機械語の実行アドレスのみ call absolute ( )に記述します。
'Mccall0.bas ' Practice of Calling machine
language--Display character [1] DATA B2, 31 : 'mov dl, 31h '----お) |
あ) まず、機械語をメモリーのどこに格納するのか、これが問題です。Basicが使うエリアとだぶっては、
暴走してしまいます。一般的には、clear 文 で機械語の領域を確保しますが、ここでは配列
a%(20) を
宣言して、40byte確保し、そこに機械語を格納することにしました。
ちなみに、a% は整数の配列なので、1つの配列で2byteメモリーを確保します。
い) DEF SEG=VARSEG(a%(0)) とすることにより、セグメントアドレスを配列の先頭、すなわち
a%(0)
のあるセグメントアドレスに設定し、以下
配列 a% で確保した領域に機械語データを読み込みます。
機械語データの先頭アドレスは VARPTR(a%(0))です。
う) 3つの文がREM文(コメント)になっていますが、 ' をとるとキーボードで入力した文字を出力する
プログラムになります。このプログラムで表示する「1」は、実は お)のDATA文の2番目の
31 です。
(「1」のアスキーコードは31h) う)3の poke 文で31の代わりにキーボードで入力した文字のアスキーコード
をいれるようになっています。あとでご自分で改変して試してみて下さい。
え) Basicから機械語に渡す引数はありませんので、機械語の実行アドレスのみ記述します。
このプログラムの機械語は先頭から実行するようになっていますので、機械語の先頭アドレス
すなわち VARPTR(a%(0)) を記述します。
お) 機械語は「1」を画面に表示するものです。 int というのは interapt (割り込み)の命令です。
int 21h はMS-DOSが用意してくれているシステムコールの割り込みです。
最後に、どうやって機械語からBasicに復帰するか、これが最後の問題です。QBasicでは
retf でBasicに戻ります。
(2) 値渡しの引数
今度はBasicから、整数を値渡しで、機械語に渡すプログラムです。上の(1)を少し改良して、
押したキーをアスキーコードとして機械語に渡し、機械語は受けた値をキャラクタとして画面に出力
するプログラムとしました。
まず、基礎知識としてデータの渡しかたについて簡単に説明します。
値渡しというのは、渡したい値(のコピー)を引数に渡すことです。値渡しの他には、(3)でやりますが
参照渡しというのがあります。参照渡しというのは、値(のコピー)を渡すのではなく、値の格納されている
アドレスを渡すやり方です。
例えば35という値を渡したいとき、この値が1015番地にあるとします。そして受ける方では、2050番地
でうけるとします。このばあい下記のように考えて下さい
番地 | 値渡し | 参照渡し |
1015 | 35-- | 35 |
・・・・ | | | |
・・・・ | | | |
2050 | 35<-- | 15 |
2051 | 10 | |
・・・・ |
値渡しでは、35という値そのものを2050番地にコピーして渡します。一方、参照渡しでは35という
データのある番地を2050、2051番地に渡します。従って、値渡しの場合、コピーされたの2050番地の
35を変更して他の数値にしても、オリジナルの1015番地はそのままということになります。
これは、どういうことかというと、Basicから機械語に値渡しで渡したデータ(35)を機械語側で
いじっても、Basic側の値は変わらない(35のまま)ということです。
それでは、Basicから値渡しで、変数を機械語に渡すプログラムです。
' MCCALL1.bas 'Practice of Calling machine language--Display Character 'Coded by Tsuyoshi Kasai for QBASIC DIM a%(20) DEF SEG = VARSEG(a%(0)) FOR i = 0 TO 13 READ d$ POKE VARPTR(a%(0)) + i, VAL("&h" + d$) NEXT st: CLS PRINT "Practice of Calling machine language--Display character" PRINT "Coded by Tsuyoshi Kasai": PRINT PRINT "Segment & Offset address of machine languege" PRINT HEX$(VARSEG(a%(0))), HEX$(VARPTR(a%(0))) PRINT "Please press any key ": c$ = INPUT$(1) '----あ) c% = ASC(c$) PRINT "Display above character by calling machine language==> "; CALL absolute(BYVAL c%, VARPTR(a%(0))) '−−−い) PRINT : PRINT "Onec More? (y/n)" DATA 55: 'push bp −−−う) |
あ) キーボード入力した1文字を C%=ASC(C$)
でアスキーコード(整数)に変換し、機械語ルーチン
に渡します。
い) 値渡しの場合は、変数の前に BYVAL (=By Value "値で"の意味)をつけます。
う) さて、Basicから渡された値は、どこに格納されて、機械語はどこでうけとればよいのでしょうか。
これが、Basicから機械語への最大のポイントです。
QBasicでは、call absolute でBasicから渡された値(引数)は、スタックにつまれます。従って、機械語
はスタックを調べて、Basicからの値をうけとればよいということになります。
このプログラムでは、以下のようにスタックにつまれます。SP+7から+2までは、call
absoluteにより
Basic側でスタックにつみ、SP, SP+1は呼ばれた機械語ルーチンの最初の命令 push
bp でつんで
います。なお、機械語実行アドレスはスタックにはつまれないようです。
SP | BP下位アドレス |
SP+1 | BP上位アドレス |
+2 | リターンアドレス |
+3 | リターンアドレス |
+4 | リターンアドレス |
+5 | リターンアドレス |
+6 | C%の値下位 |
+7 | C%の値上位 |
+8 | 機械語が呼ばれる前のSP |
Basicから渡された引数 C%はSP+6,SP+7につまれていますので、ここから値をとればよいわけです。
mov bp, sp でBP=SPとし
mov dx, [bp+6] でDXにC%の値(BP+6, BP+7の値)をいれています
この機械語はDXにセットした値をアスキーコードとみなして、画面に表示するものです。
(*注)
なお、N88Basic(86)の同様な機械語コールの命令 call の場合、引数の格納されているアドレス
(Offset,Segment)を示すデータの先頭番地が DS,BXにセットされます。
え) retf 2 は2byteすてて戻るという命令です。retf でSPはリターンアドレス(SP+5)まで戻ります。
あと、C%の値2byte分捨てさせます。これで、SPが機械語が呼ばれる前のところに戻ります。
この捨てるバイト数を間違えると、コンピューターが暴走しますので、気をつけてください。
(3) 引数の参照渡し
Basicから渡された値(変数)を機械語側で加工して、またBasicに戻したいということは、よくあること
です。さて、このような場合はどうしたらいいでしょうか。値渡しの場合、受けた引数を機械語ルーチンで
加工して、またBasicに戻すということができません。なぜなら、値渡しでもらった値(変数)は、先に説明した
ようにコピーなので、もらった値(変数)を変更しても、Basicのオリジナルの値には反映されませんので。
では、どうすればよいでしょうか。
Basic側でその値(変数)を格納しているアドレスを、機械語側に教えれば、O.K.ですね。
これがまさに、参照渡しで値を渡すということです。
いかのプログラムはBasicから渡された値に、機械語で+1してBasicに戻すプログラムです。
' MCCALL3.bas 'Practice of calling machine
language---ADD 1 CALL absolute(x, VARPTR(a%(0))) '−−−あ) DATA 55: 'push bp −−−い) |
あ) 変数x は参照渡しで渡されています。
い) スタックは以下のようになっています。参照渡しの場合、引数の格納されているオフセットアドレス
がスタックにつまれます。セグメントアドレス(DS)は自動的に引数の格納されているセグメントアドレス
がセットされるようです。
SP | BP下位アドレス |
+1 | BP上位アドレス |
+2 | リターンアドレス |
+3 | リターンアドレス |
+4 | リターンアドレス |
+5 | リターンアドレス |
+6 | xの格納されているオフセットアドレス下位 |
+7 | xの格納されているオフセットアドレス上位 |
機械語で 引数 x の値を取得し +1 するのに以下のようにしています。
mov bp, sp BP=SPとする
mov si, [bp+6] BP+6, +7(すなわち x のアドレス) をSIにセットする。
mov ax, [si] AXにSIの示すアドレスの値(すなわちx の値)をいれる
inc ax AX=AX+1する
mov [si], ax AXの値をSIの示すアドレスの値(すなわちx)に入れる
さて、機械語で直接 Basic側に格納されている x の値を変更したので、Basicに戻って
x を
みてみると、+1されているはずです。
結構、盛りだくさんの内容になりましたが、いかがだったでしょうか。機械語そのものの説明は不十分
だったと思いますが、他のページでとりあげていく予定なので、そちらも見てください。
5 おまけ
とりあえず、整数をBasicから機械語に渡す例だけ、取り上げてみましたが、あと実数や文字列
の問題があります。以下に筆者が、機械語へ渡されるデータがスタックにどのように、つまれるのか、
調べるのに使ったプログラムを掲載します。
たいへんな出来損ないですが、これを参考に各自もっといいものを作って、いろいろ調べて下さい。
原理としては、機械語で配列a%(0)+30以降にスタック領域のデータを(たった)4byte分コピーし
Basicに戻ってから、peek で読んで表示させるというプログラムです。(おそまつ)
あとは、皆さんで改良して下さい。
' MCCALL2.bas '*Practice of calling machine
language* DATA 55 : ' push bp |
実行中のスタックの状態は以下の通りです。SPとBPにが違うところを差していますので
注意して下さい。
SP | pushされたDS | |
+1 | pushされたDS | |
BP | +2 | BP下位アドレス |
+1 | +3 | BP上位アドレス |
+2 | +4 | リターンアドレス |
+3 | +5 | リターンアドレス |
+4 | +6 | リターンアドレス |
+5 | +7 | リターンアドレス |
+6 | +8 | xの下位値 |
+7 | +9 | xの上位値 |
+8 | +10 | xの格納されているオフセットアドレス下位 |
+9 | +11 | xの格納されているオフセットアドレス上位 |
+10 | +12 | aseg(a%(0)のセグメントアドレス)の下位値 |
+11 | +13 | aseg(a%(0)のセグメントアドレス)の下位値 |
あ) ここでは、スタック領域のどこを機械語で読み取るかを入力します。例えば6を入力すると、
上記の表のBP+6〜BP+9の4バイト分を読み取り、表示します。
い) call absolute でaseg(a%(0)のセグメントアドレス)、xを参照渡し、xを値渡し、機械語開始番地を
引数にしています。
う) 配列a%の30〜33番目のデータを読み取っています。あ)で指定したスタック領域4バイト分が、機械語
でコピーされています。
わかりにくいコードなので、若干解説しておきます。
push bp
mov bp,sp BP=SP
push ds DSをpush
mov ds,[bp+10] 配列a%のセグメントアドレスをDSにセット
mov si,30 SIに30をセット-->配列a%の30番目をさす
mov ax,[bp+6] デフォルトでは[BP+6]だが、この6のところに、あ)で指定した値が入っている。
mov [si],ax 配列a%の30,31番目の要素にAXの値をコピー
mov ax,[bp+8] [BP+8]の8のところに、あ)で指定した数値+2の値が入っている
mov [si+2],ax 配列a%の32,33番目の要素にAXの値をコピー
pop ds
pop bp
retf 6
自分の研究用に作ったものなので、めちゃくちゃわかりにくいプログラムになってます。
あとで、自分で見ても理解するのに1時間かかりました。皆様ごめん。