パソコン活用研究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では、心配無用のようです。



Basic活用研究目次
TOPPAGE