BASICから機械語へ part3 機械語(マシン語)の呼び出し call absolute (

 パソコン活用研究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]
' Coded by tsuyoshi Kasai for QBASIC
DIM a%(20)    '---あ)
CLS
PRINT "Practice of Calling machine language--Display character [1] "
PRINT "Coded by Tsuyoshi Kasai": PRINT
DEF SEG = VARSEG(a%(0))    '----い)
FOR i = 0 TO 6
READ d$
POKE VARPTR(a%(0)) + i, VAL("&h" + d$)
NEXT
PRINT "Segment & offset address of machine languege"
PRINT HEX$(VARSEG(a%(0))), HEX$(VARPTR(a%(0)))
'INPUT "input 1 character"; c$    '----う)1
'c$ = LEFT$(c$, 1)            '----う)2
PRINT "Display character[1] by calling machine language==> ";
'POKE VARPTR(a%(0)) + 1, ASC(c$)   '----う)3   
CALL absolute(VARPTR(a%(0)))      '----え)

DATA B2, 31 :    'mov dl, 31h        '----お)
DATA B4, 02 :    'mov ah, 02h
DATA CD, 21 :    'int 21h
DATA cb        'retf

あ) まず、機械語をメモリーのどこに格納するのか、これが問題です。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)"
loop1:
c$ = INPUT$(1)
IF c$ = "Y" OR c$ = "y" THEN GOTO st
IF c$ = "N" OR c$ = "n" THEN END
GOTO loop1

DATA 55:     'push bp        −−−う)
DATA 89,e5 :   'mov bp,sp
DATA 8b,56,6 :  'mov dx,[bp+6]
DATA b4,2 :    'mov ah,2
DATA cd,21 :   'int 21h
DATA 5d :     'pop bp
DATA ca,2,0:    'retf 2      −−−−え)
DATA 0

あ) キーボード入力した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
'Coded by Tsuyoshi Kasai for QBASIC

DIM a%(20)
DEFINT A-Z
DEF SEG = VARSEG(a%(0))
FOR i = 0 TO 14
READ d$
POKE VARPTR(a%(0)) + i, VAL("&h" + d$)
NEXT
st:
CLS
PRINT "Practice of Calling machine language---ADD 1"
PRINT "Coded by Tsuyoshi Kasai": PRINT
INPUT "please input any integer less than 32000 :", x
PRINT "calling machine language -- ADD 1 ==>results ";

CALL absolute(x, VARPTR(a%(0)))   '−−−あ)
PRINT x
PRINT : PRINT "Onec More? (y/n)"
loop1:
c$ = INPUT$(1)
IF c$ = "Y" OR c$ = "y" THEN GOTO st
IF c$ = "N" OR c$ = "n" THEN END
GOTO loop1

DATA 55:     'push bp   −−−い)
DATA 89,e5:   'mov bp,sp
DATA 8b,76,6:   'mov si,[bp+6]
DATA 8b,4:     'mov ax,[si]
DATA 40:      'inc ax
DATA 89,4:     'mov [si],ax
DATA 5d:      'pop bp
DATA ca,2,0:    'retf 2

あ) 変数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*
'*Check stack Coded by Tsuyoshi kasai for QBASIC
DIM a%(20)
DEFINT A-Z
DEF SEG = VARSEG(a%(0))
FOR i = 0 TO 25
READ d$
POKE VARPTR(a%(0)) + i, VAL("&h" + d$)
NEXT
st:
CLS
PRINT "Practice of Calling machine language--check stack coded by Tsuyoshi kasai"
PRINT
PRINT "Segment & Offset address of machine languege's begin(=address of a%() )"
PRINT "SEG "; HEX$(VARSEG(a%(0))), "OFF "; HEX$(VARPTR(a%(0)))
INPUT "please input any integer less than 65000 to var x ", x
PRINT "Var X Segment address "; HEX$(VARSEG(x)); " offset address "; HEX$(VARPTR(x))
inpstack:
INPUT "No.of stack for checking (more than 0) ", stack '---あ)
IF stack < 1 THEN GOTO inpstack
POKE VARPTR(a%(0)) + 12, stack: POKE VARPTR(a%(0)) + 17, stack + 2
PRINT "Call machine language"
aseg = VARSEG(a%(0))
CALL absolute(BYVAL aseg, BYVAL x%, x%, VARPTR(a%(0))) ' ---い)
PRINT
PRINT "*stacks*"
Voff = VARPTR(a%(0))
FOR i = 30 TO 33
PRINT HEX$(PEEK(Voff + i)) + " "; '----う)
NEXT
PRINT : PRINT "Onec More? (y/n)"
loop1:
c$ = INPUT$(1)
IF c$ = "Y" OR c$ = "y" THEN GOTO st
IF c$ = "N" OR c$ = "n" THEN END
GOTO loop1

DATA 55 : ' push bp
DATA 89,e5 : 'mov bp,sp
DATA 1e : 'push ds
DATA 8e,5e,a : 'mov ds,[bp+10]
DATA be,1e,0: 'mov si,30
DATA 8b,46,6 : 'mov ax,[bp+6]
DATA 89,4 : 'mov [si],ax
DATA 8b,46,8: 'mov ax,[bp+8]
DATA 89,44,2: 'mov [si+2],ax
DATA 1f : 'pop ds
DATA 5d : 'pop bp
DATA ca,6,0 : 'retf 6
DATA 0

実行中のスタックの状態は以下の通りです。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時間かかりました。皆様ごめん。


TOP PAGE >Basic活用研究目次