日本の方ならみなさん持っているであろう国民機、PC-9800シリーズ(以下PC-98)でプログラム(コンピュータ)を勉強したいと思います。
今回はとりあえずPC-98本体内臓のbeep音を鳴らすことが目的なので、難しい話は流して行きます。
準備
Windows環境(XP以上)で開発していきます。
まずはツールの準備から。
【アセンブラ】
まずプログラムはアセンブリ言語で書いていくのでアセンブラを用意しましょう。
今回はNASMを選択しました。
NASMのページから最新版zipをダウンロードしてください。
(投稿段階のWindows_32bit用最新版のリンク貼っておきます。→ nasm-2.13.03-win32.zip)
【バイナリエディタ】
アセンブルされた機械語を読むためにバイナリエディタを使いました。
とりあえずBZエディタを用意します。
Vectorダウンロードページ
BZ
BZの詳しい使い方はこちらの記事がオススメです。
Binary Editor BZ
【PC-98エミュレータ】
この記事を読んでくださっている方の中には**もしかしたら、**PC-98実機を持っていない方がいらっしゃるかもしれないので、PC-98のエミュレータを用意します。
私は「Neko Project II」を選びました。
「Neko Project II」のページから自分のPC環境にあったものをダウンロードします。
(Windows_32bitの方は"WinNT"を選んでください。)
前提
CPUとアセンブラの関係
アセンブラとは人間の読みやすい(たぶん)アセンブリ言語をCPUが理解できる機械語(0と1の集まり)に翻訳するものです。
しかしながら、CPUにもたくさん種類があり、それぞれ機能や理解できる機械語に違いがあります。(方言のようなもの)
なので、アセンブラはいきなりアセンブリコードを渡されて「翻訳して」と言われても、どのCPU向けの機械語に翻訳していいかわからず、うまく翻訳できないことがあります。
よって、今回はどのCPU向けのコードを書くか前提条件を決めましょう。
前提条件
PC-98のCPUは「intel 8086」から始まり、シリーズの世代によって搭載されているものは違いますが、今回エミュレータとして使用する「Neko Project II」のデフォルト環境は「intel 80286」相当なのでこれを前提にします。
とにかくBeepを鳴らす
まずは以下のコードをコピペして「hello98_beep.asm」の名前で保存します。
; hello98_beep.asm
; Intel記法
[BITS 16]
[CPU 286]
; 以下は1.23MBフォーマット(通称: NEC PC-98フォーマット)フロッピーディスクのための記述
; 現在の標準的なフロッピーフォーマットは1.44MBフォーマットで、若干違うので注意
; 参考までにコメント括弧内に ( PC98(1.23MB) : PC/AT(1.44MB) ) 形式で比較を書いた
JMP SHORT entry
DB 0x90
DB "HELLO98 " ; ブートセクタの名前を自由に書いてよい(8バイト)
DW 1024 ; 1セクタの大きさ(1024 : 512)
DB 1 ; クラスタの大きさ(1セクタ)
DW 1 ; FATがどこから始まるか(普通は1セクタ目からにする)
DB 2 ; FATの個数(2にしなければいけない)
DW 192 ; ルートディレクトリ領域の大きさ(192 : 普通は224エントリにする)
DW 1440 ; このドライブの大きさ(1440セクタ : 2880セクタ)
DB 0xfe ; メディアのタイプ(0xfe : 0xf0)
DW 2 ; FAT領域の長さ(2セクタ : 9セクタ)
DW 8 ; 1トラックにいくつのセクタがあるか(8セクタ : 18セクタ)
DW 2 ; ヘッドの数(2にしなければいけない)
DD 0 ; パーティションを使ってないのでここは必ず0
DD 1440 ; このドライブ大きさをもう一度書く
DB 0x00 ; フロッピーディスクでは0x00、固定ディスクでは0x80
DB 0 ; WindowsNT予約領域。常に0を設定すべき。
DB 0x29 ; 拡張ブートシグネチャ。下の3つの設定が存在することを示す。
DD 0xffffffff ; ボリュームシリアル番号(IDは大抵現在時刻から生成されるらしい)
DB "HELLO98 " ; ディスクの名前(11バイト)
DB "FAT12 " ; フォーマットの名前(8バイト)
;/////////////////////////////
; プログラム本体
;/////////////////////////////
section .text
entry:
XOR AX,AX ; レジスタ初期化
;-------------------------------------------------
;ビープ音を鳴らす
beep:
;CRT BIOSの機能を使う(BIOS-59)
MOV AH, 0x17
INT 0x18 ; ブザーON
;-------------------------------------------------
;終了
fin:
HLT ; 何かあるまでCPUを停止させる
JMP fin ; 無限ループ
;-------------------------------------------------
TIMES 0x400-($-$$) DB 0 ; 0x400までを0x00で埋める命令
では、これ(hello98_beep.asm)をアセンブルしましょう。
$ nasm -f bin -o hello98_beep.bin hello98_beep.asm
無事アセンブルできると「hello98_beep.bin」というファイルが作られます。
そしたら、以下のページから「hood」ファイルをダウンロードして、「hello98_beep.bin」と同じフォルダに入れます。
hood(GitHub)
そして以下のコマンドを実行しましょう。
$ copy /B hello98_beep.bin + hood hello98_beep.TFD
「Neko Project II」を起動して、フロッピードライブ( [FDD1]→[Open...] )に「hello98_beep.TFD」を指定して [Emulate]→[Reset] すると無事、beep音が鳴ると思います!(ちゃんと実機でも鳴りました)
永遠となり続けるので、止めるときは「Neko Project II」を閉じましょう。
何をやったの?(解説)
さっきのコードとちょっとした作業でどうしてbeepを鳴らすことができたのか、順を追って見ていきます。
ディレクティブ
まずは上から見ていきます。
; hello98_beep.asm
; Intel記法
[BITS 16]
[CPU 286]
;
から始まっている行はコメントです。
次に出てくる[]
で囲われたものはアセンブラへの命令。
前提でお話したように、アセンブラはコードをどんなCPU向けに翻訳していいかわかりません。
なのでこの[]
で囲われた命令でどんなCPU向けに翻訳するか指定しています。
今回は**「Intel 80286向けの16bit命令で翻訳してね」と指定しています。
このようなアセンブラやコンパイラへの明示的な指定は「ディレクティブ」**と呼ばれ、C言語だと「#include」なんかがディレクティブです。
ブートセクタとBPB
次に、ここ
; 以下は1.23MBフォーマット(通称: NEC PC-98フォーマット)フロッピーディスクのための記述
; 現在の標準的なフロッピーフォーマットは1.44MBフォーマットで、若干違うので注意
; 参考までにコメント括弧内に ( PC98(1.23MB) : PC/AT(1.44MB) ) 形式で比較を書いた
JMP SHORT entry
DB 0x90
DB "HELLO98 " ; ブートセクタの名前を自由に書いてよい(8バイト)
DW 1024 ; 1セクタの大きさ(1024 : 512)
DB 1 ; クラスタの大きさ(1セクタ)
DW 1 ; FATがどこから始まるか(普通は1セクタ目からにする)
DB 2 ; FATの個数(2にしなければいけない)
DW 192 ; ルートディレクトリ領域の大きさ(192 : 普通は224エントリにする)
DW 1440 ; このドライブの大きさ(1440セクタ : 2880セクタ)
DB 0xfe ; メディアのタイプ(0xfe : 0xf0)
DW 2 ; FAT領域の長さ(2セクタ : 9セクタ)
DW 8 ; 1トラックにいくつのセクタがあるか(8セクタ : 18セクタ)
DW 2 ; ヘッドの数(2にしなければいけない)
DD 0 ; パーティションを使ってないのでここは必ず0
DD 1440 ; このドライブ大きさをもう一度書く
DB 0x00 ; フロッピーディスクでは0x00、固定ディスクでは0x80
DB 0 ; WindowsNT予約領域。常に0を設定すべき。
DB 0x29 ; 拡張ブートシグネチャ。下の3つの設定が存在することを示す。
DD 0xffffffff ; ボリュームシリアル番号(IDは大抵現在時刻から生成されるらしい)
DB "HELLO98 " ; ディスクの名前(11バイト)
DB "FAT12 " ; フォーマットの名前(8バイト)
とっても長いですがここは理解というよりお決まりですので「ふーんそうなんだ」ぐらいでサラサラ見ていきましょう。
まず、今回はフロッピーにプログラムを書き込んで実行することを想定しています。
なのでPCはフロッピーが差し込まれたら 「お、フロッピー差し込まれたな!」→「書かれてる命令を読むわ!」→「実行するわ!」 という具合に動いてくれるのが理想です。
しかし実際には 「お、フロッピー差し込まれたな!」 の後に問題があります。
「これどうやって読むん・・・? プログラム書いてる場所どこ・・・?」とフロッピーの読み方がわからないのです。
これはフロッピーに限らず、USBでもHDでも同じで、自分がどういう構成なのかPCに教えてあげなければなりません。
この「自分はこういう構成だよー」というパラメータのまとまりをBPB(BIOS Parameter Block)そしてそれが記録されている場所をブートセクタと呼びます。
また、何も設定されていないまっさらな記録媒体(フロッピーとか)に、ファイルシステムを構成し、BPBを書き込む作業をフォーマットと言います。
さて、*「サラサラ見ていきましょう」*と言った割に説明がくどくなってきましたね。。。
本当は構成の仕方にも色々(FAT12とかFAT32とか...)あるのですが、大体のパラメータの説明はコメントで書いてありますし、今回はBeepを鳴らすことが大きな目的ですので割愛させていただきます。
FATについて気になった方は下記のページで詳しく解説されています。オススメです。
最後に大事な部分を一つだけ、先頭にあるJMP SHORT entry
の命令。
これがあることでPCは次に解説するプログラムに飛ぶことができます。
要は「プログラムが書かれてるのは"entry"からなのね」とパソコンが理解するわけです。
beepを鳴らすプログラム(BIOS)
レジスタの初期化
さて、いよいよプログラムのメイン部分を見ていきます。
と、その前にここを見ます
section .text
entry:
XOR AX,AX ; レジスタ初期化
section .text
とは「ここからがプログラムだよ」という区切り。
そしてentry:
はラベルと呼ばれる区切りで、先ほどのJMP SHORT entry
のおかげでPCはここからプログラムを読み始めます。
で、XOR AX,AX
がレジスタの初期化。
レジスタとはCPUが計算に必要な値を記憶したり、計算結果を代入したりとCPUと値をやり取りするために使う箱です。
やっていることは「PCを起動した段階ではここに何が入っているかわから
ないので、0で初期化する」という作業です。
さて、 "XOR hoge,fuga" ですがこれはXORの名前通り排他的論理和を計算します。
排他的論理和は「hogeとfugaの値が一緒だったら絶対0」なので、XOR AX,AX
とした場合、AX同士の値は絶対同じなので必ず0になります。
**「普通にAXに0代入すればいいじゃん(MOV AX,0)」**と思うかもしれませんが、XORの方が実行時間が早く、メモリも節約できるのでよく使われています。
beepを鳴らす
レジスタを綺麗にしたところで今度こそメインプログラムです。
;ビープ音を鳴らす
beep:
;CRT BIOSの機能を使う(BIOS-59)
MOV AH, 0x17
INT 0x18 ; ブザーON
・・・これだけです。
「beepを鳴らせ」と命令しているのはコメントとラベルを抜かせばたった2行です。
一応見ていきましょう、
MOV AH, 0x17
これはAHというレジスタに0x17という値を代入しています。
INT 0x18
これはBIOS割り込みルーチンの0x18を実行しています。
・・・さて、何を言っているのかさっぱりかもしれませんが、まずBIOS割り込みルーチンとはとてもざっくり言えば、「BIOSに記録されている、ハードウェアを制御するためのとても小さな関数」です。
で、その関数の呼び出し方なのですが例えばPC-98だと「AHに0x17を入れてINT 0x18を実行するとブザーが鳴る」と、決められています。
こればっかりは仕様書を読みながらどんな機能があって、どうやったら呼び出せるか調べていくしかありません。
PC-98の場合BIOSの機能については以下の書籍に全てまとめられています。(書籍買うのはちょっと・・・って人はググると情報出てくるかも・・・?)
今回はこのようなBIOSの便利機能を使ったのでたった2行でブザーを鳴らすことができたというわけです。
ちなみに、
MOV AH, 0x18
INT 0x18 ; ブザーOFF
とするとブザーを止めることができます。
プログラムの終わり
;終了
fin:
HLT ; 何かあるまでCPUを停止させる
JMP fin ; 無限ループ
;-------------------------------------------------
TIMES 0x400-($-$$) DB 0 ; 0x400までを0x00で埋める命令
プログラムの終わりはfin:
からです。
HLT
は英語のhalt
の略で、CPUを停止させる(アイドル状態にさせる)命令です。
正直HLT
がなくても見た目動きに変わりはないのですが、実はこれがないとCPUは常に全力投球で、CPU負荷は100%となります。つまり何にもしてないのに空回り状態です。
それを防ぐためにHLT
命令を実行し、JMP fin
でラベルfin:
に戻る無限ループを実行します。
ほんとのホントの最後はTIMES 0x400-($-$$) DB 0
これは0x400バイト(10進じゃないことに注意! 0x400は10進だと1,024バイト)まで0
で埋めてという命令です。
確認のために先ほどアセンブルしたhello98_beep.bin
をBZエディタで見てみましょう。
BZエディタを起動して見たいファイルをドラック&ドロップするとバイナリが表示されます。
0x400バイトまで00
で埋められていることが確認できます。
その他
アセンブル
今回使った以下のコマンド。
$ nasm -f bin -o hello98_beep.bin hello98_beep.asm
とてもとても簡単な構文はこの通り。
$ nasm -f bin -o [出力ファイル] [入力ファイル]
-f bin
とは「バイナリ(機械語)として出力してね。」というオプション。
バイナリ結合
$ copy /B hello98_beep.bin + hood hello98_beep.TFD
これはファイルをコピーするコマンド(参照)だが、コピー元ファイルの指定をfile1 + file2
と+
でつなぐことでそれらのファイルを結合できる。
とてもとても簡単な構文はこの通り。
$ copy /B [コピー元ファイル] [コピー先ファイル]
/B
とは「バイナリファイルとして扱ってね」というオプション。
これがないとテキストデータとして扱われてファイルの最後に0x1A(EOF)
を自動で付け足されたりする。
"hood"
バイナリ結合が理解できたところで、「じゃあくっつけてるhoodはなんなんだ・・・?」となります。
今回想定しているフロッピーディスクは「3.5インチ2HD 両面高密度倍トラック」で物理的な容量は1.6MB
ですがPC-98では一般的に1,261,568バイト
までフォーマットして使用するそうです。(参照:フロッピーディスクの規格一覧表)
1,261,568バイト
は16進で0x134000バイト
ですが、先ほどアセンブルしたhello98_beep.bin
は0x400
までしか書き込んでいません。
本来は0x134000バイト
までフォーマットデータが書き込まれるのですが、手書きするのは面倒くさいですし今回は事前にファイルで用意して結合しました。
まとめ
たかがbeepを鳴らすだけでしたが、その道のりを0から一つずつ見ていくとかなり密度の濃いものとなりました。
今回やった内容はコンパイラが違ったり、細かい部分で修正されていなかったりしますが以下のGitHubリポジトリにまとめています。
https://github.com/TakedaHiromasa/HelloWorld-PC98
ぜひさらに興味の湧いた人は「FATの規格について」や「ブートローダ・ブートストラップ」についてより詳しく調べてみるといいかもしれません。
次回からはbeepの音階を変えたり、画面に「Hello world」を表示したりする記事を書こうかなと思ってます。
私事ですが、今回初めてQiitaの記事を書きましたがめちゃくちゃ書くの大変でした・・・
これからもペースが遅いとは思いますがちまちま記事を書いていこうかなと思います。
おまけ
PC-98の実機でこのプログラムを実行したい人は、2HDフロッピーを先に実機のMS-DOSなどでフォーマットし、
「Read/Write FD」などを使い、3モードフロッピードライブでTFDファイルを直書きすると実機で実行できます。