パソコン活用研究5番街(Visual Basic、Excel(VBA)、BASIC プログラミング研究)
旧BASIC−−BASIC(PEEK,POKE)からマシン語へ、メモリーへのアクセス、パソコン探索(QB)
1 めもりーについて(基礎)
Z80,6809といった8ビットCPUがアクセスできたメモリーは、0000H〜FFFFH(Hは16進数
を表す)。すなわち、64Kバイトです。PENTIUMの基本?となった8086という16ビットCPUが
アクセスできたのは、00000H〜FFFFFH、すなわち1Mバイトです。
8ビットCPUの場合、メモリーのアドレス(番地)は単純に3455Hのように指定すればよく、例え
ば、アセンブラで、JMP
3000H(6809アセンブラのジャンプ命令)というのは、3000H番地
へジャンプせよという、ことでした。
いっぽう、8086では番地の指定には、オフセットアドレス、セグメントアドレスという2つの
アドレスで指定されることとなりました。指定の仕方は、
番地=(セグメントアドレス*16)+オフセットアドレス
ということになります。(セグメントアドレス、オフセットアドレスともに0000HからFFFFHまで
となります。)
面倒なことですが、A5000Hのようには直接指定できません。
8086のJMP 命令は、JMP
オフセットアドレス (JMP
5600H など)
という記述になります。
今仮に、セグメントアドレス(以下SEGと略す)が0000Hの時に、JMP
1000Hということは、
実際の番地は、0H*16+1000H=1000Hとなります。
SEGが2000Hで JMP 1000Hなら
実際の番地は、2000H*16+1000H=20000H+1000H=21000Hとなります。
(*16進数で2000H*16(=10H)は20000Hです。)
SEGを指定するには、セグメントレジスタというレジスタを使います。8086では、主に
CS(コードセグメント)・・・実行コードのあるセグメントを指定する。
DS(データセグメント)・・・データのあるセグメントを指定する。
というセグメントレジスタを使います。
さて、賢明な方はお気づきの通り、この番地の指定の仕方の場合、同じ番地を指定するのに
いくとおりでもやり方があることになります。例えば、100Hを指定したい場合
SEG=10H
オフセットアドレス=0Hでも、SEG=0H
オフセットアドレス=100H
でも良いわけです。従って、下手にCS,DSを設定すると、データ領域とコード領域が重なって、
データでマシン語コードをつぶして、暴走ということもありえるわけです。
ちなみに、286、386、486、PENTIUMでは、1Mバイト以上のメモリーをアクセスできるよう
になりましたが、メモリーの番地の指定は複雑になり、簡単にはアクセスできなくなりました。
ここでは説明いたしません。
2 PEEK,POKE
Basicである程度プログラムできるようになると、なんとなく機械語(=マシン語=CPUが直接
解釈できるコード)でコーディングしたくなるものです。(というのは昔の話だろうか)
今は、C/C++をやりたくなるというのが定石かな。
ま、それはいいとして、むかーし「準備なしに機械語を始めると、奇怪語?に遭遇して、カルチャーショックをうけますよ」なんて冗談がありましたが、いきなりアセンブラというのは・・・、という
ご要望にこたえて、とりあえずBASICから機械語へとアプローチしてみましょう。
なお、BASICとはいっても、各種ありますが、WINDOWS上で再現可能ということで、主として
QBASICを使って、話を進めます。
第1部のメモリーの話はO.K.でしょうか。あと、機械語を覗くのに必要な基礎知識として、
HEX$,VALをおさらいしておきましょう。
HEX$・・・10進数を16進数の文字変数に変換する関数です。HEX$(17)=11
VAL・・・・16進数(文字列)を10進数の数値変数に変換する関数。
VAL(”&H11)=17
PEEK (アドレス)
メモリーの値を読み出す関数です。
A=PEEK(&H4000)
とすると、変数Aに&H4000番地の値が読みこまれます。
ただし、8ビットCPUのBasicの場合は、この通りですが、上述の通り、8086以上
のパソコンの場合は、DEF
SEG=セグメントアドレス、PEEK(オフセットアドレス)という
指定の仕方になります。
(なお、N88互換BASIC for
Winでは、PEEK,POKEはサポートしてません。
Windows上で直接番地を指定してメモリーの値を読む、というのはできないと思って
下さい。)
<N88BASIC(86)用>
10 DEF SEG=&H5000
20 A=PEEK(&H500)
30 PRINT HEX$(A)
というプログラムでセグメントアドレス&H5000、オフセットアドレス&H500番地の値が
16進数で表示できます。QBASICでも行番号をとれば、これでO.K.
それでは、まとめてメモリーを覗くプログラムを作ってみましょう。
基本的には
<N88BASIC(86)用>
Qbasicは行番号をとればO.K.
10 DEF
SEG=&H1000
’ここにはいろんなセグメントアドレスを書く
20 FOR I=0 TO &HFFFF
30 PRINT
HEX$(PEEK(I))+” ”;
40 NEXT
でSEG=&H1000から64Kバイト分(&HFFFF)表示するプログラムになります。
しかし、これでは、ダッーと表示していってしまい、見にくいので、少し工夫したプログラム
を以下にのせます。
<QBASIC用>
'Memory Dump of Qbasic written by Kasai DIM q(20) st: CLS PRINT "Memory Dump of Qbasic written by Kasai" PRINT "64 KB memory dump" PRINT "Press 1 Key to suspend Press 2 Key to stop" INPUT "SEGment address : ", s$ DEF SEG = VAL("&h" + s$) PRINT " + 0 1 2 3 4 5 6 7 8 9 A B C D E F" FOR i = 0 TO &HFFF segadd = (VAL("&h" + s$) + i) * 16 PRINT RIGHT$("0000" + HEX$(segadd) + ":", 6); FOR j = 0 TO 15 q(j) = PEEK(i * 16 + j) PRINT RIGHT$("0" + HEX$(q(j)) + " ", 3); NEXT j FOR j = 0 TO 15 IF q(j) < &H20 THEN ch$ = " " ELSE ch$ = CHR$(q(j)) PRINT ch$; NEXT j PRINT
e$ = INKEY$ IF e$ = "1" THEN PRINT "Hit return key to start again" INPUT r$ PRINT " + 0 1 2 3 4 5 6 7 8 9 A B C D E F" END IF IF e$ = "2" THEN EXIT FOR
NEXT i PRINT "Continue C End E" L1: w$ = INPUT$(1) IF w$ = "C" THEN GOTO st IF w$ = "E" THEN END ELSE GOTO L1
最初にセグメントアドレスを聞いてきますので、セグメントアドレスを入力して下さい。
ダッーと64Kバイト表示していきますので、途中でいったん停止したければ、1
を押して
下さい。再開はリターンキーです。 2
を押すと終了です。
このQBASICによるプログラムは、MS−DOSプロンプト、MS−DOSモードで動きます
ので、いろいろなアドレスを表示させてみて下さい。
ただし、WINDOWSにおける、MS−DOSプロンプトは、MS−DOSのエミュレート(MS−DOS
のふりをしている)です。上述のように、Windowsは32ビットCPUで動いてますので、
実際のアドレス指定はもっと複雑です。このプログラムで、SEGとオフセットアドレスの指定
だけで、PEEK関数で読んでいるメモリーは、Windowsが、MS−DOSのフリをして、
うまいこと機能させているというわけです。(ま、とりあえずこれでよしとしましょう)
もちろん、フロッピーから純然たるMS−DOSや起動ディスクから立ち上げても動きます。
実際に、PEEK関数を使った簡単なプログラム例。
以下のプログラムはPC2001のメモリーを読んで、RS232Cで外部にデータを送信する
プログラムです。実際、筆者はこれで、PC2001のメモリーを読んで、Windows機に
データ転送し、Windows機上で組んだPC2001用ディスアセンブラで、PC2001の
ROM解析や、PC2001中間コード変換プログラムで、中間コード==>テキストプログラム
変換などやりました。(参考
RS232Cプロジェクト)
メモリー上のバイナリデータを文字データに変換してRS232Cに送信するぷろぐらむ。
メモリー上の&HE000から&HE300までのデータを送信します。
&HE000からはBASICプログラムが中間コードの形で入ってます。
10 Term J,0,4
’データ長8ビット、パリティ無し、1200ボー
20 For I=&HE000 to &HE300
30 D=Peek(I) ’メモリーのバイナリデータを読む
40 Print %1,HEX$(D);
’文字型に変換してRS232Cから送信
50 next
POKE
POKE
アドレス、数値
PEEK関数でメモリーを読みましたが、逆にメモリーに書き込むのが、POKE関数です。
例えば、
SEG=&H7000
POKE &H300, &H21
と書けば、SEG=&H7000,オフセットアドレス
&H300に&H21を書きこみます。
PRINT
HEX$(PEEK(&H300))
で逆に読み出して確かめて見てください。
ただし、POKEで書き込む時に、システムが使用している領域に変な数を書き込んでしまう
とコンピューターが暴走してしまいますので、そこらへんは、よくマニュアル等で調べて下さい。
V−RAM書き込み
V−RAMのVは、VideoのVです。すなわち、CRTに出力するデータがある所です。
昔のパソコンの多くはメインRAM上の一部をV−RAMとして割り当てていたため、
POKE関数で直接V−RAMにデータを書き込むことにより、画面にデータを出力できました。
こうすると、LOCATE X,Y: PRINT
”*”というお決まりのやり方より、スピードアップに
なったため、昔のBASICプログラム(アマチュア作の)でよく使われていました。
そういう技をつかうと、BASICプログラマーとして、そろそろ中級かとコメントされたものです。
MS−DOSプロンプト、MS−DOSモードのメモリーマップ等に関する資料がないため、
試行錯誤でいろいろやってみたところ、PC/AT(DOS/V)機のMS−DOSプロンプト、
MS−DOSモードでは
&HC8000からがテキストV−RAMのようなので、そこにPOKE関数
で書き込むプログラムを作ってみました。(PC−98シーリズではだめだと思います)
<QBASIC>
DEF SEG = &HB800
st:
CLS
PRINT "V-RAM Access Practice by QBasic"
PRINT "written by Tsuyoshi Kasai"
INPUT "COLOR NO. (smaller than 255)", c
c = c MOD 256
FOR i = 0 TO 256
POKE i * 2, i MOD 256
POKE i * 2 + 1, c
NEXT
PRINT "Once More---(Y/N)"
loop1:
a$ = INKEY$
IF a$ = "Y" THEN GOTO st
IF a$ = "N" THEN END
GOTO loop1
これは、V−RAMに直接書き込むことにより、画面に表示させるプログラムです。
実行するとこんな感じです。
さて、V−RAMへの書き込みをPOKE関数で行うと速い、といいましたが、ついでに
その実験をしてみます。
SAMPLE−1は、799回ひたすら、POKEでキャラクタを書き込むプログラムです。
SAMPLE−2は同じ事を、PRINT文でやっています。
プログラム開始と終了時にTimeを計測して表示させます。
SAMPLE−1とSAMPLE−2では、筆者のパソコンのMS−DOSプロンプトでは、
共に、約0.05秒で大差ありません。POKEを使うことに意味があるのでしょうか。
(筆者の記憶だと、昔のふるーいBASICではPRINT文はかなり遅くなったはずだが
まあ、BASICの違いでいろいろあるので)
<QBASIC用>
SAMPLE−1
CLS
DEF SEG = &HB800
t = TIMER
FOR i = 0 TO 799
POKE i * 2 , &H40
NEXT
t1 = TIMER
LOCATE 17, 1
PRINT "time": PRINT USING "##.####"; t1 - t
SAMPLE−2
CLS
DEF SEG = &HB800
t = TIMER
FOR i = 0 TO 799
PRINT "h";
NEXT
t1 = TIMER
LOCATE 17, 1
PRINT "time": PRINT USING "##.####"; t1 - t
それでは、キャラクタを移動させるプログラムも試してみます。
SAMPLE−3ではPOKEだけを使っています。SAMPLE−4では、同じ事を、LOCATE
とPRINT でやっています。
結果はSAMPLE−3では約0.1秒。SAMPLE−4では約0.2秒。
ということで、めでたくPOKE で直接V−RAMに書き込む方が速いという結果になりました。
(最近のパソコンでは、画面の書き換えよりキャラクタの移動の方が圧倒的に速いので、
実行しても良く分からないかもしれません。それで、わざとウェイトをかけて遅くしたのが
SAMPLE−5です。これで試して見てください。ちなみに筆者のパソコンでは約3分かかります)
SAMPLE−3
CLS
DEF SEG = &HB800
x = 0
t = TIMER
FOR i = 0 TO 799
POKE x, &H20
POKE i * 2, &H40
x = i * 2
NEXT
t1 = TIMER
LOCATE 17, 1
PRINT "time": PRINT USING "##.####"; t1 - t
SAMPLE−4
CLS
DEF SEG = &HB800
x = 1: y = 1
t = TIMER
FOR i = 1 TO 10
FOR j = 1 TO 79
LOCATE y, x: PRINT " ";
LOCATE i, j: PRINT "*";
y = i: x = j
NEXT
NEXT
t1 = TIMER
LOCATE 17, 1
PRINT "time": PRINT USING "##.####"; t1 - t
SAMPLE−5
DEF SEG = &HB800
x = 1: y = 1
t = TIMER
FOR i = 1 TO 10
FOR j = 1 TO 79
FOR k=0 TO
4000 : NEXT
’ここでWAITをかけている。
LOCATE y, x: PRINT " ";
LOCATE i, j: PRINT "*";
y = i: x = j
NEXT
NEXT
t1 = TIMER
LOCATE 17, 1
PRINT "time": PRINT USING "##.####"; t1 - t
おまけで、今までQbasicで話をしてきましたが、試しに同様のプログラムを
N88互換BASIC for win
と、筆者の最古のパソコンFM−8(F−Basic
Ver2.0)で
試してみます。
(N88互換BASIC for Win
FM−8+F-BASIC 用)
10 width 80,20
20 cls
25 x=0:y=0
30 t=val(right$(time$,2))
40 for i=0 to 9
50 for j=0 to 79
60 locate x,y: print " "
70 locate j,i:print "q"
80 x=j:y=i
90 next j
95 next i
100 t1=val(right$(time$,2))
110 if t1<t then t1=t1+60
120 print t1-t
N88互換BASIC for
Winは16ビット用のものを使用しました。(32ビット用の最新
ヴァージョンはもっと速いと思いますが)。平均28秒でした。
FM−8 + F−Basic Ver2.0では平均35秒でした。
どちらも、ウェイト無しでキャラクタの動きを目でおえました。QBASICと100倍以上のスピード
差がついています。
FM−8など使っていた時代は、スピードアップのための工夫というのが、非常に重要でしたが、
QBASICでは、心配無用のようです。